# Copyright 2021-2024 The DeepCAVE Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# noqa: D400
"""
# Objective
This module provides utilities to convert and create objectives.
It also provides functions for merging and comparing objectives.
## Classes
- Objective: Convert and create objectives.
"""
from typing import Any, Dict, Optional, Union
from dataclasses import dataclass
import numpy as np
from deepcave.runs.exceptions import NotMergeableError
[docs]
@dataclass
class Objective:
"""
Convert, create and merge objectives.
Properties
----------
lower : Optional[Union[int, float]]
The lower bound of the objective.
upper : Optional[Union[int, float]]
The upper bound of the objective.
optimize : str
Define whether to optimize lower or upper.
lock_lower : bool
Whether to lock the lower bound.
lock_upper : bool
Whether to lock the upper bound.
name : str
The name of the objective.
"""
name: str
lower: Optional[Union[int, float]] = None
upper: Optional[Union[int, float]] = None
optimize: str = "lower"
[docs]
def __post_init__(self) -> None:
"""
Check if bounds should be locked.
Lock the lower bound if lower is not None.
Lock the upper bound if upper is not None.
Additionally, sets self.lower to np.inf if it is None and self.upper to -np.inf if it is
None, resulting in an empty bound.
Therefore, the values will be updated as soon as configurations are added.
Raises
------
RuntimeError
If optimize is not `lower` or `upper`.
"""
if self.lower is None:
lock_lower = False
self.lower = np.inf
else:
lock_lower = True
if self.upper is None:
lock_upper = False
self.upper = -np.inf
else:
lock_upper = True
if self.optimize != "lower" and self.optimize != "upper":
raise RuntimeError("`optimize` must be 'lower' or 'upper'")
self.lock_lower = lock_lower
self.lock_upper = lock_upper
[docs]
def to_json(self) -> Dict[str, Any]:
"""
Convert objectives attributes to a dictionary in a JSON friendly format.
Returns
-------
Dict[str, Any]
A dictionary in a JSON friendly format with the objects attributes.
"""
return {
"name": self.name,
"lower": self.lower,
"upper": self.upper,
"lock_lower": self.lock_lower,
"lock_upper": self.lock_upper,
"optimize": self.optimize,
}
[docs]
@staticmethod
def from_json(d: Dict[str, Any]) -> "Objective":
"""
Create an objective from a JSON friendly dictionary format.
Parameters
----------
d : Dict[str, Any]
A dictionary in a JSON friendly format containing the attributes.
Returns
-------
Objective
An objective created from the provided data.
"""
objective = Objective(
name=d["name"],
lower=d["lower"],
upper=d["upper"],
optimize=d["optimize"],
)
objective.lock_lower = d["lock_lower"]
objective.lock_upper = d["lock_upper"]
return objective
[docs]
def __eq__(self, other: Any) -> bool:
"""
Compare if two instances are equal based on their attributes.
Parameters
----------
other : Any
The other instance to compare.
Returns
-------
bool
True if equal, else False.
"""
attributes = ["name", "lock_lower", "lock_upper", "optimize"]
for a in attributes:
if getattr(self, a) != getattr(other, a):
return False
return True
[docs]
def merge(self, other: Any) -> None:
"""
Merge two Objectives over their attributes.
Parameters
----------
other : Any
The other Objective to merge.
Raises
------
NotMergeableError
If parts of the two Objectives are not mergeable.
ValueError
If the lower bound of one Objective is None.
If the upper bound of one Objective is None.
"""
if not isinstance(other, Objective):
raise NotMergeableError("Objective can only be merged with another Objective.")
attributes = ["name", "lock_lower", "lock_upper", "optimize"]
for attribute in attributes:
if getattr(self, attribute) != getattr(other, attribute):
raise NotMergeableError(f"Objective {attribute} can not be merged.")
if self.lower is None or other.lower is None:
raise ValueError("The lower bound of one Objective is None.")
if self.upper is None or other.upper is None:
raise ValueError("The upper bound of one Objective is None.")
if self.lock_lower and self.lock_lower == other.lock_lower:
if self.lower != other.lower:
raise NotMergeableError(f"Objective {other.name}'s lower bound can not be merged.")
else:
if self.lower > other.lower:
self.lower = other.lower
if self.lock_upper and self.lock_upper == other.lock_upper:
if self.upper != other.upper:
raise NotMergeableError(f"Objective {other.name}'s upper bound can not be merged.")
else:
if self.upper < other.upper:
self.upper = other.upper
[docs]
def get_worst_value(self) -> Optional[Union[int, float]]:
"""
Get the worst value based on the optimization setting.
Returns
-------
float
The worst value based on the optimization setting.
"""
if self.optimize == "lower":
return self.upper
else:
return self.lower