from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
from negmas.common import PreferencesChange
from negmas.helpers import snake_case
from negmas.helpers.types import get_full_type_name
from negmas.outcomes import Outcome
from negmas.outcomes.common import check_one_at_most, os_or_none
from negmas.serialization import PYTHON_CLASS_IDENTIFIER, deserialize, serialize
from negmas.types import NamedObject
__all__ = ["Preferences"]
if TYPE_CHECKING:
from negmas import Rational
from negmas.outcomes.base_issue import Issue
from negmas.outcomes.common import Outcome
from negmas.outcomes.protocols import OutcomeSpace
[docs]
class Preferences(NamedObject, ABC):
"""
Base class for all preferences.
Args:
outcome_space: The outcome-space over which the preferences are defined
"""
outcome_space: OutcomeSpace | None
reserved_outcome: Outcome
owner: Rational | None = None
def __init__(
self,
*args,
outcome_space: OutcomeSpace | None = None,
issues: tuple[Issue] | None = None,
outcomes: tuple[Outcome] | int | None = None,
reserved_outcome: Outcome | None = None,
**kwargs,
) -> None:
self.owner = None
check_one_at_most(outcome_space, issues, outcomes)
super().__init__(*args, **kwargs)
self.outcome_space = os_or_none(outcome_space, issues, outcomes)
self.reserved_outcome = reserved_outcome # type: ignore
self._changes: list[PreferencesChange] = []
[docs]
@abstractmethod
def is_volatile(self):
"""
Does the utility of an outcome depend on factors outside the negotiation?
Remarks:
- A volatile preferences is one that can change even for the same mechanism state due to outside influence
"""
[docs]
@abstractmethod
def is_not_worse(self, first: Outcome | None, second: Outcome | None) -> bool:
"""
Is `first` at least as good as `second`
"""
...
[docs]
@abstractmethod
def is_session_dependent(self):
"""
Does the utility of an outcome depend on the `NegotiatorMechanismInterface`?
"""
[docs]
@abstractmethod
def is_state_dependent(self):
"""
Does the utility of an outcome depend on the negotiation state?
"""
[docs]
def to_dict(self) -> dict[str, Any]:
d = {PYTHON_CLASS_IDENTIFIER: get_full_type_name(type(self))}
return dict(
**d,
outcome_space=serialize(self.outcome_space),
reserved_outcome=self.reserved_outcome,
name=self.name,
id=self.id,
)
[docs]
@classmethod
def from_dict(cls, d):
d.pop(PYTHON_CLASS_IDENTIFIER, None)
d["outcome_space"] = deserialize(d.get("outcome_space", None))
return cls(**d)
[docs]
def is_stationary(self) -> bool:
"""Are the preferences stationary (i.e. repeated calls return the same value for any preferences comparion or evaluaton method)?"""
return (
not self.is_state_dependent()
and not self.is_volatile()
and not self.is_session_dependent()
)
[docs]
def changes(self) -> list[PreferencesChange]:
"""
Returns a list of changes to the preferences (if any) since last call.
Remarks:
- If the ufun is stationary, the return list will always be empty.
- If the ufun is not stationary, the ufun itself is responsible for saving the
changes in _changes whenever they happen.
"""
if self.is_stationary():
return []
r = [_ for _ in self._changes]
self.reset_changes()
return r
[docs]
def reset_changes(self) -> None:
"""
Will be called whenever we need to reset changes.
"""
self._changes = []
@property
def type(self) -> str:
"""Returns the utility_function type.
Each class inheriting from this ``UtilityFunction`` class will have its own type. The default type is the empty
string.
Examples:
>>> from negmas.preferences import *
>>> from negmas.outcomes import make_issue
>>> print(LinearAdditiveUtilityFunction((lambda x:x, lambda x:x), issues=[make_issue((0, 1), (0, 1))]).type)
linear_additive
>>> print(MappingUtilityFunction([lambda x: x], issues=[make_issue((0.0, 1.0))]).type)
mapping
Returns:
str: utility_function type
"""
return snake_case(
self.__class__.__name__.replace("Function", "").replace("Utility", "")
)
@property
def base_type(self) -> str:
"""Returns the utility_function base type ignoring discounting and similar wrappings."""
from .discounted import DiscountedUtilityFunction
u = self
while isinstance(u, DiscountedUtilityFunction):
u = u.ufun
return self.type
[docs]
def is_better(self, first: Outcome | None, second: Outcome | None) -> bool:
"""
Compares two offers using the `ufun` returning whether the first is strictly better than the second
Args:
first: First outcome to be compared
second: Second outcome to be compared
Remarks:
- Should raise `ValueError` if the comparison cannot be done
"""
return self.is_not_worse(first, second) and not self.is_not_worse(second, first)
[docs]
def is_equivalent(self, first: Outcome | None, second: Outcome | None) -> bool:
"""
Compares two offers using the `ufun` returning whether the first is strictly equivelent than the second
Args:
first: First outcome to be compared
second: Second outcome to be compared
Remarks:
- Should raise `ValueError` if the comparison cannot be done
"""
return self.is_not_worse(first, second) and self.is_not_worse(second, first)
[docs]
def is_not_better(self, first: Outcome, second: Outcome | None) -> bool:
"""
Compares two offers using the `ufun` returning whether the first is worse or equivalent than the second
Args:
first: First outcome to be compared
second: Second outcome to be compared
Remarks:
- Should raise `ValueError` if the comparison cannot be done
"""
return self.is_not_worse(second, first)
[docs]
def is_worse(self, first: Outcome | None, second: Outcome | None) -> bool:
"""
Compares two offers using the `ufun` returning whether the first is strictly worse than the second
Args:
first: First outcome to be compared
second: Second outcome to be compared
Remarks:
- Should raise `ValueError` if the comparison cannot be done
"""
return not self.is_not_worse(first, second)