Skip to content

Commit

Permalink
Merge pull request #307 from sebastian-echeverria/feature/value-updates
Browse files Browse the repository at this point in the history
Feature/condition updates
  • Loading branch information
turingcompl33t authored Dec 6, 2023
2 parents 1f4703b + 7cbc526 commit fb90540
Show file tree
Hide file tree
Showing 15 changed files with 194 additions and 59 deletions.
4 changes: 1 addition & 3 deletions demo/simple/confusion_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,7 @@ def misclassifications(self) -> int:

@classmethod
def misclassification_count_less_than(cls, threshold: int) -> Condition:
condition: Condition = Condition(
"misclassification_count_less_than",
[threshold],
condition: Condition = Condition.build_condition(
lambda cm: Success(
f"Misclass count {cm.misclassifications} less than threshold {threshold}"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
"callback": {
"title": "Callback",
"type": "string"
},
"value_class": {
"title": "Value Class",
"type": "string"
}
},
"required": [
"name",
"callback"
"callback",
"value_class"
],
"title": "ConditionModel",
"type": "object"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
"callback": {
"title": "Callback",
"type": "string"
},
"value_class": {
"title": "Value Class",
"type": "string"
}
},
"required": [
"name",
"callback"
"callback",
"value_class"
],
"title": "ConditionModel",
"type": "object"
Expand Down
8 changes: 2 additions & 6 deletions mlte/measurement/cpu/local_process_cpu_utilization.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,7 @@ def max_utilization_less_than(cls, threshold: float) -> Condition:
:return: The Condition that can be used to validate a Value.
"""
condition: Condition = Condition(
"max_utilization_less_than",
[threshold],
condition: Condition = Condition.build_condition(
lambda stats: Success(
f"Maximum utilization {stats.max:.2f} "
f"below threshold {threshold:.2f}"
Expand All @@ -127,9 +125,7 @@ def average_utilization_less_than(cls, threshold: float) -> Condition:
:return: The Condition that can be used to validate a Value.
"""
condition: Condition = Condition(
"average_utilization_less_than",
[threshold],
condition: Condition = Condition.build_condition(
lambda stats: Success(
f"Average utilization {stats.max:.2f} "
f"below threshold {threshold:.2f}"
Expand Down
8 changes: 2 additions & 6 deletions mlte/measurement/memory/local_process_memory_consumption.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ def max_consumption_less_than(cls, threshold: int) -> Condition:
:return: The Condition that can be used to validate a Value.
:rtype: Condition
"""
condition: Condition = Condition(
"max_consumption_less_than",
[threshold],
condition: Condition = Condition.build_condition(
lambda stats: Success(
f"Maximum consumption {stats.max} "
f"below threshold {threshold}"
Expand All @@ -135,9 +133,7 @@ def average_consumption_less_than(cls, threshold: float) -> Condition:
:return: The Condition that can be used to validate a Value.
:rtype: Condition
"""
condition: Condition = Condition(
"average_consumption_less_than",
[threshold],
condition: Condition = Condition.build_condition(
lambda stats: Success(
f"Average consumption {stats.avg} "
f"below threshold {threshold}"
Expand Down
7 changes: 6 additions & 1 deletion mlte/schema/artifact/spec/v0.0.1/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
"callback": {
"title": "Callback",
"type": "string"
},
"value_class": {
"title": "Value Class",
"type": "string"
}
},
"required": [
"name",
"callback"
"callback",
"value_class"
],
"title": "ConditionModel",
"type": "object"
Expand Down
7 changes: 6 additions & 1 deletion mlte/schema/artifact/validated/v0.0.1/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
"callback": {
"title": "Callback",
"type": "string"
},
"value_class": {
"title": "Value Class",
"type": "string"
}
},
"required": [
"name",
"callback"
"callback",
"value_class"
],
"title": "ConditionModel",
"type": "object"
Expand Down
77 changes: 62 additions & 15 deletions mlte/spec/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
from __future__ import annotations

import base64
import inspect
import typing
from typing import Any, Callable, List
from typing import Any, Callable, List, Type

import dill

Expand All @@ -28,14 +29,15 @@ def __init__(
name: str,
arguments: List[Any],
callback: Callable[[Value], Result],
value_class: str = "",
):
"""
Initialize a Condition instance.
:param name: The name of the name method, for documenting purposes.
:type name: str
:param callback: The callable that implements validation
:type callback: Callable[[Value], Result]
:param arguments: The list of arguments passed to the callable.
:param callback: The callable that implements validation.
:param value_class: The full module + class name of the Value that generated this condition.
"""

self.name: str = name
Expand All @@ -47,66 +49,111 @@ def __init__(
self.callback: Callable[[Value], Result] = callback
"""The callback that implements validation."""

self.value_class: str = (
value_class
if value_class != ""
else f"{Value.__module__}.{Value.__name__}"
)
"""Value type class where this Condition came from."""

def __call__(self, value: Value) -> Result:
"""
Invoke the validation callback
:param value: The value of measurement evaluation
:type value: Value
:return: The result of measurement validation
:rtype: Result
"""
return self.callback(value)._with_evidence_metadata(value.metadata)

@staticmethod
def build_condition(test: Callable[[Value], Result]) -> Condition:
# Get info about the caller from inspection.
curr_frame = inspect.currentframe()
if curr_frame is None:
raise Exception("Unexpected error reading validation method data.")
caller_function = curr_frame.f_back
if caller_function is None:
raise Exception("Unexpected error reading validation method data.")

# Get function name and arguments of callers.
validation_name = caller_function.f_code.co_name
arguments = caller_function.f_locals

# Build the class info as a string.
if "cls" not in arguments:
raise Exception(
"'cls' argument is needed in validation method arguments."
)
cls: Type[Value] = arguments["cls"]
cls_str = f"{cls.__module__}.{cls.__name__}"

# Validation args include all caller arguments except for the value class type.
validation_args = []
for arg_key, arg_value in arguments.items():
if arg_key != "cls":
validation_args.append(arg_value)

condition: Condition = Condition(
validation_name, validation_args, test, cls_str
)
return condition

def to_model(self) -> ConditionModel:
"""
Returns this condition as a model.
:return: The serialized model object.
:rtype: ConditionModel
"""
return ConditionModel(
name=self.name,
arguments=self.arguments,
callback=base64.b64encode(dill.dumps(self.callback)).decode(
"utf-8"
),
callback=Condition.encode_callback(self.callback),
value_class=self.value_class,
)

@staticmethod
def encode_callback(callback: Callable[[Value], Result]) -> str:
"""Encodes the callback as a base64 string."""
return base64.b64encode(dill.dumps(callback)).decode("utf-8")

@classmethod
def from_model(cls, model: ConditionModel) -> Condition:
"""
Deserialize a Condition from a model.
:param model: The model.
:type model: ConditionModel
:return: The deserialized Condition
:rtype: Condition
"""
condition: Condition = Condition(
model.name,
model.arguments,
dill.loads(base64.b64decode(str(model.callback).encode("utf-8"))),
model.value_class,
)
return condition

def __str__(self) -> str:
"""Return a string representation of Condition."""
return f"{self.name}"
return f"{self.name} ({self.arguments}) from {self.value_class}"

# -------------------------------------------------------------------------
# Equality Testing
# -------------------------------------------------------------------------

def __eq__(self, other: object) -> bool:
"""Compare Condition instances for equality."""
# TODO: is just names enough? Should we compare args and callback?
if not isinstance(other, Condition):
return False
reference: Condition = other
return self.name == reference.name
return (
self.name == reference.name
and Condition.encode_callback(self.callback)
== Condition.encode_callback(other.callback)
and self.arguments == other.arguments
and self.value_class == other.value_class
)

def __neq__(self, other: Condition) -> bool:
"""Compare Condition instances for inequality."""
Expand Down
3 changes: 3 additions & 0 deletions mlte/spec/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class ConditionModel(BaseModel):
callback: str
"""A text-encoded, dilled-serialized version of the callback to execute when validating this condition."""

value_class: str
"""A string indicating the full module and class name of the Value used to generate this condition."""


class PropertyModel(BaseModel):
"""A description of a property."""
Expand Down
4 changes: 1 addition & 3 deletions mlte/value/types/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,7 @@ def ignore(cls, reason: str) -> Condition:
:param reason: The reason for ignoring the image
:return: The Condition that can be used to validate a Value.
"""
condition: Condition = Condition(
"Ignore",
[reason],
condition: Condition = Condition.build_condition(
lambda _: Ignore(reason),
)
return condition
10 changes: 3 additions & 7 deletions mlte/value/types/integer.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,14 @@ def less_than(cls, value: int) -> Condition:
:return: The Condition that can be used to validate a Value.
:rtype: Condition
"""
condition: Condition = Condition(
"less_than",
[value],
condition: Condition = Condition.build_condition(
lambda integer: Success(
f"Integer magnitude {integer.value} less than threshold {value}"
)
if integer.value < value
else Failure(
f"Integer magnitude {integer.value} exceeds threshold {value}"
),
)
)
return condition

Expand All @@ -120,9 +118,7 @@ def less_or_equal_to(cls, value: int) -> Condition:
:return: The Condition that can be used to validate a Value.
:rtype: Condition
"""
condition: Condition = Condition(
"less_or_equal_to",
[value],
condition: Condition = Condition.build_condition(
lambda integer: Success(
f"Integer magnitude {integer.value} "
f"less than or equal to threshold {value}"
Expand Down
20 changes: 5 additions & 15 deletions mlte/value/types/real.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ def less_than(cls, value: float) -> Condition:
:return: The Condition that can be used to validate a Value.
:rtype: Condition
"""
condition: Condition = Condition(
"less_than",
[value],
condition: Condition = Condition.build_condition(
lambda real: Success(
f"Real magnitude {real.value} less than threshold {value}"
)
Expand All @@ -118,9 +116,7 @@ def less_or_equal_to(cls, value: float) -> Condition:
:return: The Condition that can be used to validate a Value.
:rtype: Condition
"""
condition: Condition = Condition(
"less_or_equal_to",
[value],
condition: Condition = Condition.build_condition(
lambda real: Success(
f"Real magnitude {real.value} "
f"less than or equal to threshold {value}"
Expand All @@ -143,9 +139,7 @@ def greater_than(cls, value: float) -> Condition:
:return: The Condition that can be used to validate a Value.
:rtype: Condition
"""
condition: Condition = Condition(
"greater_than",
[value],
condition: Condition = Condition.build_condition(
lambda real: Success(
f"Real magnitude {real.value} greater than threshold {value}"
)
Expand All @@ -167,16 +161,12 @@ def greater_or_equal_to(cls, value: float) -> Condition:
:return: The Condition that can be used to validate a Value.
:rtype: Condition
"""
condition: Condition = Condition(
"greater_or_equal_to",
[value],
condition: Condition = Condition.build_condition(
lambda real: Success(
f"Real magnitude {real.value} "
f"greater than or equal to threshold {value}"
)
if real.value >= value
else Failure(
f"Real magnitude {real.value} below threshold {value}"
),
else Failure(f"Real magnitude {real.value} below threshold {value}")
)
return condition
Loading

0 comments on commit fb90540

Please sign in to comment.