Skip to content

Commit e4acec3

Browse files
feat: add run_experiment script (#77)
* refactor: Update to property model Aim is to not include PropertyDomain objects where not required reducing size of models and schemas. Another aim is to fix recursion in ExperimentReference due to the PropertyValue model having field that can be ObservedPropertyValue, which in turn will include an Experiment Reference Changes: - Add PropertyDescriptors: variants of Property types that do not have a domain or metadata - Change models that only require Descriptors to use them (Entity,PropertyValue) - Allow models that take PropertyDescriptors to still be initialised with Properties (converting them) for flexibility and compatibility - Update tests - Add additional conversions where required * chore: add types * feat: Specific PropertyValue subclasses for Constitutive, Observed and Virtual properties You cannot use the base PropertyValue for Observed or Virtual property values * refactor: Update schema models to use new PropertyValue types. In particular update input and return types to strictly declare which type they use (in particular to take/use ConstitutivePropertyValue if this is expected not PropertyValue which is compatible but generic) * test: Update schema tests and fixtures to new PropertyValue subclasses * refactor: Update other orchestrator code to new PropertyValue types * test: Update other orchestrator tests and fixtures to new PropertyType * refactor: Update sfttrainer and related tests to new PropertyType models * refactor: Update raytune operator to new PropertyType models * refactor: Update examples to new PropertyType subclasses * refactor: Change experiment target properties to be descriptors * refactor: Change experiment targetProperty field type to descriptors * fix: Missing __str__ method * chore: remove stray prints * chore: remove stray prints * fix: models of incorrect type raise validationerror immediately The before validator is never called * refactor: remove concretePropertyIdentifiers field Never used. * fix: revert change in before validator This class method can also be called directly in which case it can convert a Property instance to a descriptor * test: measurement_for_entity * chore: update comment * chore: revert part of 5ea1e98 Keep concretePropertyIdentifiers field in AbstractProperty as otherwise we break stored models. * fix: property_to_descriptor for AbstractPropertyDescriptor * Apply suggestion from @AlessandroPomponio Co-authored-by: Alessandro Pomponio <[email protected]> Signed-off-by: Michael Johnston <[email protected]> * Apply suggestion from @AlessandroPomponio Co-authored-by: Alessandro Pomponio <[email protected]> Signed-off-by: Michael Johnston <[email protected]> * feat: add validate_point_against_properties function A base function for comparing a set of "identifier:value" pairs against a set of constitutive properties. * refactor: use validate_point_against_properties code in isPointInSpace is basis of new function * refactor: move SpacePoint to schema Also change experiment field to be list of ExperimentReferences. * feat: add validate_entity * chore: add classmethod decorator Co-authored-by: Alessandro Pomponio <[email protected]> Signed-off-by: Michael Johnston <[email protected]> * feat: add check against optional properties * feat: add run_experiment script * chore: typing * fix: not accessing items * chore: tidy * chore: missing headers * fix: non-deterministic results from nevergrad optimization test function by default translation is 1.0 which randomly translates the space of the ArtificialFunction. Multiple calls to the experiment then were actually are using different randomly translated functions. * feat: remote execution and typer * docs(website): add run_experiment docs * refactor: rename wrappers * feat: handle various errors from remote call * feat: improve typer usage * chore: formatting * fix: remove unnecessary code * fix: missing return * chore: use timedelta * docs(examples): example point.yaml * test: add test of run_experiment * docs: improvements * chore: missing copyright header * fix: allow int or string debug level --------- Signed-off-by: Michael Johnston <[email protected]> Co-authored-by: Alessandro Pomponio <[email protected]>
1 parent 34d2077 commit e4acec3

File tree

13 files changed

+509
-61
lines changed

13 files changed

+509
-61
lines changed

examples/optimization_test_functions/custom_experiments/ado_actuators/optimization_test_functions/optimization_test_functions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def artificial_function(
3030
block_dimension=int(
3131
len(entity.constitutive_property_values) / parameters["num_blocks"]
3232
),
33+
translation_factor=0.0,
3334
)
3435

3536
# Call the nevergrad function
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright (c) IBM Corporation
2+
# SPDX-License-Identifier: MIT
3+
4+
entity:
5+
x0: 1
6+
x1: 2
7+
x2: -1
8+
experiments:
9+
- actuatorIdentifier: custom_experiments
10+
experimentIdentifier: nevergrad_opt_3d_test_func

orchestrator/cli/models/space.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22
# SPDX-License-Identifier: MIT
33

44
import math
5-
import typing
65
from io import StringIO
7-
from typing import Annotated
86

9-
import pydantic
107
import rich.table
118

129
import orchestrator.metastore.project
@@ -430,16 +427,3 @@ def to_dataframe(
430427
columns_to_hide=columns_to_hide,
431428
)
432429
)
433-
434-
435-
class SpacePoint(pydantic.BaseModel):
436-
entity: Annotated[
437-
dict[str, typing.Any] | None,
438-
pydantic.Field(
439-
description="A dictionary of property names and values to be searched."
440-
),
441-
] = None
442-
experiments: Annotated[
443-
list[str] | None,
444-
pydantic.Field(description="A list of experiments to be searched."),
445-
] = None

orchestrator/cli/resources/discovery_space/get.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from rich.status import Status
99

1010
from orchestrator.cli.models.parameters import AdoGetCommandParameters
11-
from orchestrator.cli.models.space import SpacePoint
1211
from orchestrator.cli.models.types import (
1312
AdoGetSupportedOutputFormats,
1413
)
@@ -32,6 +31,7 @@
3231
)
3332
from orchestrator.core.resources import CoreResourceKinds
3433
from orchestrator.schema.entityspace import EntitySpaceRepresentation
34+
from orchestrator.schema.point import SpacePoint
3535

3636
if typing.TYPE_CHECKING:
3737
import pandas as pd
@@ -118,10 +118,14 @@ def _find_spaces_matching_point(
118118
raise
119119

120120
experiment_selectors = []
121-
for experiment in target_point.experiments or []:
121+
for reference in target_point.experiments or []:
122122
experiment_selectors.extend(
123123
prepare_query_filters_for_db(
124-
{"config.experiments": {"experiments": {"identifier": experiment}}}
124+
{
125+
"config.experiments": {
126+
"experiments": {"identifier": reference.experimentIdentifier}
127+
}
128+
}
125129
)
126130
)
127131

orchestrator/schema/entityspace.py

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
from orchestrator.schema.domain import VariableTypeEnum
77
from orchestrator.schema.entity import Entity
88
from orchestrator.schema.property import ConstitutiveProperty
9-
from orchestrator.schema.property_value import constitutive_property_values_from_point
9+
from orchestrator.schema.property_value import (
10+
constitutive_property_values_from_point,
11+
validate_point_against_properties,
12+
)
1013
from orchestrator.schema.result import MeasurementResult
1114

1215

@@ -224,52 +227,20 @@ def isPointInSpace(
224227
Dictionary representing the point with constitutive property identifiers as keys and
225228
constitutive property values as values.
226229
:param allow_partial_matches:
227-
If True, ill return True if at least one property matches; otherwise the point must
228-
match all the constitutive properties of the entities.
230+
If True, all key:values in point must have a matching constitutive property in the
231+
entity space and be in its domain.
232+
If False (default), in addition to above, all constitutive properties in the entity space
233+
must have a matching key in point
229234
230235
:return: True if the point is in the space, False otherwise.
231236
"""
232-
constitutive_property_identifiers_for_point = set(point.keys())
233-
constitutive_property_identifiers_for_entity_space = {
234-
cp.identifier for cp in self._constitutiveProperties
235-
}
236237

237-
matching_constitutive_property_identifiers = (
238-
constitutive_property_identifiers_for_point.intersection(
239-
constitutive_property_identifiers_for_entity_space
240-
)
238+
return validate_point_against_properties(
239+
point=point,
240+
constitutive_properties=self._constitutiveProperties,
241+
allow_partial_matches=allow_partial_matches,
241242
)
242243

243-
# If we don't allow partial matches, all properties must match
244-
if not allow_partial_matches and len(
245-
matching_constitutive_property_identifiers
246-
) != len(constitutive_property_identifiers_for_entity_space):
247-
return False
248-
249-
# If we allow partial matches, all properties for the point must
250-
# match
251-
if len(matching_constitutive_property_identifiers) != len(
252-
constitutive_property_identifiers_for_point
253-
):
254-
return False
255-
256-
# Once we have checked that the identifiers match, we must
257-
# check that the values specified by the point are in the domain
258-
# of the constitutive properties.
259-
for constitutive_property in self.constitutiveProperties:
260-
if (
261-
constitutive_property.identifier
262-
not in matching_constitutive_property_identifiers
263-
):
264-
continue
265-
266-
if not constitutive_property.propertyDomain.valueInDomain(
267-
point[constitutive_property.identifier]
268-
):
269-
return False
270-
271-
return True
272-
273244
def isEntityInSpace(self, entity: Entity):
274245
"""Returns True if entity is in the space otherwise false
275246

orchestrator/schema/experiment.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# SPDX-License-Identifier: MIT
33

44
import importlib.metadata
5+
import logging
56
import typing
67
from typing import Annotated
78

@@ -18,7 +19,11 @@
1819
MeasuredPropertyTypeEnum,
1920
Property,
2021
)
21-
from orchestrator.schema.property_value import ConstitutivePropertyValue, PropertyValue
22+
from orchestrator.schema.property_value import (
23+
ConstitutivePropertyValue,
24+
PropertyValue,
25+
validate_point_against_properties,
26+
)
2227
from orchestrator.schema.reference import (
2328
ExperimentReference,
2429
check_parameterization_validity,
@@ -615,6 +620,74 @@ def propertyValuesFromEntity(self, entity: "Entity", target=False) -> dict:
615620

616621
return identifierValueMap
617622

623+
def validate_entity(self, entity: "Entity", strict_optional=False) -> bool:
624+
"""Returns True if Experiment can be applied to entity, false otherwise
625+
626+
This method only checks constitutive properties.
627+
- All properties of the Entity that match the experiments required or optional properties must have
628+
values in the domain of that property
629+
- All required properties of the experiment must have a matching constitutive property
630+
- If strict_optional is True all properties of the Entity that are not required properties of the Experiment
631+
must match optional properties of the experiment.
632+
"""
633+
634+
point = {
635+
v.property.identifier: v.value for v in entity.constitutive_property_values
636+
}
637+
if validate_point_against_properties(
638+
point,
639+
constitutive_properties=self.requiredConstitutiveProperties,
640+
):
641+
return True
642+
643+
# It's not an exact match - check if partial match
644+
if not validate_point_against_properties(
645+
point,
646+
constitutive_properties=self.requiredConstitutiveProperties,
647+
allow_partial_matches=True,
648+
):
649+
# no partial match - missing required properties or has incorrect values for them
650+
logging.getLogger("experiment").warning(
651+
f"The entity is missing or has invalid values for required properties of "
652+
f" {self.identifier}"
653+
)
654+
return False
655+
656+
# It has the required properties with valid values but there are additional properties
657+
# See if these properties are optional propertiesof the experiment
658+
potential_optional_properties: set[str] = point.keys() - {
659+
cp.identifier for cp in self.requiredProperties
660+
}
661+
optional_properties = potential_optional_properties & {
662+
cp.identifier for cp in self.optionalProperties
663+
}
664+
# If strict_optional is on all the additional properties must be optional properties
665+
if (
666+
len(optional_properties) != len(potential_optional_properties)
667+
and strict_optional
668+
):
669+
logging.getLogger("experiment").warning(
670+
f"Strict property checking is on and the following entity "
671+
f"properties are not required or optional properties of {self.identifier}:"
672+
f"{potential_optional_properties-optional_properties} "
673+
)
674+
return False
675+
676+
is_valid = validate_point_against_properties(
677+
point={key: point[key] for key in optional_properties},
678+
constitutive_properties=list(self.optionalProperties),
679+
allow_partial_matches=True,
680+
)
681+
if not is_valid:
682+
logging.getLogger("experiment").warning(
683+
f"The entity has properties that match optional properties"
684+
f"of {self.identifier} - "
685+
f"{potential_optional_properties - optional_properties} - "
686+
f"but its values for those properties are not in the domain of the optional properties"
687+
)
688+
689+
return is_valid
690+
618691

619692
class ParameterizedExperiment(Experiment):
620693
"""Represents an Experiment where default optional parameters have been overridden

orchestrator/schema/point.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright (c) IBM Corporation
2+
# SPDX-License-Identifier: MIT
3+
4+
import typing
5+
from typing import Annotated
6+
7+
import pydantic
8+
9+
from orchestrator.schema.entity import Entity
10+
from orchestrator.schema.property import ConstitutivePropertyDescriptor
11+
from orchestrator.schema.property_value import ConstitutivePropertyValue
12+
from orchestrator.schema.reference import ExperimentReference
13+
14+
15+
class SpacePoint(pydantic.BaseModel):
16+
"""A simplified representation of an Entity and an associated set of experiments"""
17+
18+
entity: Annotated[
19+
dict[str, typing.Any] | None,
20+
pydantic.Field(description="A dictionary of property name:value pairs"),
21+
] = None
22+
experiments: list[ExperimentReference] | None = (
23+
pydantic.Field(default=None, description="A list of experiments"),
24+
)
25+
26+
def to_entity(self) -> Entity:
27+
28+
return Entity(
29+
constitutive_property_values=tuple(
30+
[
31+
ConstitutivePropertyValue(
32+
value=v, property=ConstitutivePropertyDescriptor(identifier=k)
33+
)
34+
for k, v in self.entity.items()
35+
]
36+
)
37+
)

orchestrator/schema/property_value.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# SPDX-License-Identifier: MIT
33

44
import enum
5+
import typing
56

67
import pydantic
78

@@ -161,3 +162,65 @@ def constitutive_property_values_from_point(
161162
ConstitutivePropertyValue(value=point[c.identifier], property=c)
162163
for c in properties
163164
]
165+
166+
167+
def validate_point_against_properties(
168+
point: dict[str, typing.Any],
169+
constitutive_properties: list[ConstitutiveProperty],
170+
allow_partial_matches: bool = False,
171+
):
172+
"""point is valid if all its keys have a constitutive_property with
173+
a matching identifier and all its values are in the domain of this
174+
property. If allow_partial_matches is False an additional condition is that
175+
every key in point has a matching property AND vice versa
176+
177+
Params:
178+
point: A dictionary whose keys are property identifiers and values are values for those properties
179+
constitutive_properties: A list of ConstitutiveProperty instances to validate the point against
180+
allow_partial_matches: If True a point is valid if all its points have matching properties, even if
181+
there are more constitutive properties
182+
183+
Returns:
184+
- True if point is compatible with space otherwise false
185+
"""
186+
187+
constitutive_property_identifiers_for_point = set(point.keys())
188+
constitutive_property_identifiers_for_entity_space = {
189+
cp.identifier for cp in constitutive_properties
190+
}
191+
192+
matching_constitutive_property_identifiers = (
193+
constitutive_property_identifiers_for_point.intersection(
194+
constitutive_property_identifiers_for_entity_space
195+
)
196+
)
197+
198+
# If we don't allow partial matches, all properties must match
199+
if not allow_partial_matches and len(
200+
matching_constitutive_property_identifiers
201+
) != len(constitutive_property_identifiers_for_entity_space):
202+
return False
203+
204+
# If we allow partial matches, all properties for the point must
205+
# match
206+
if len(matching_constitutive_property_identifiers) != len(
207+
constitutive_property_identifiers_for_point
208+
):
209+
return False
210+
211+
# Once we have checked that the identifiers match, we must
212+
# check that the values specified by the point are in the domain
213+
# of the constitutive properties.
214+
for constitutive_property in constitutive_properties:
215+
if (
216+
constitutive_property.identifier
217+
not in matching_constitutive_property_identifiers
218+
):
219+
continue
220+
221+
if not constitutive_property.propertyDomain.valueInDomain(
222+
point[constitutive_property.identifier]
223+
):
224+
return False
225+
226+
return True

0 commit comments

Comments
 (0)