from __future__ import annotations
import random
from typing import Iterable
import numpy as np
from negmas.common import Distribution
from negmas.generics import gmap
from negmas.helpers import get_full_type_name
from negmas.helpers.prob import ScipyDistribution
from negmas.outcomes import Issue, Outcome
from negmas.outcomes.protocols import OutcomeSpace
from negmas.serialization import PYTHON_CLASS_IDENTIFIER, deserialize, serialize
from ..base import OutcomeUtilityMapping
from ..mixins import StationaryMixin
from ..prob_ufun import ProbUtilityFunction
__all__ = ["ProbMappingUtilityFunction"]
[docs]
class ProbMappingUtilityFunction(StationaryMixin, ProbUtilityFunction):
"""
Outcome mapping utility function.
This is the simplest possible utility function and it just maps a set of `Outcome`s to a set of
`Value`(s). It is only usable with single-issue negotiations. It can be constructed with wither a mapping
(e.g. a dict) or a callable function.
Args:
mapping: Either a callable or a mapping from `Outcome` to `Value`.
default: value returned for outcomes causing exception (e.g. invalid outcomes).
name: name of the utility function. If None a random name will be generated.
reserved_value: The reserved value (utility of not getting an agreement = utility(None) )
Remarks:
- If the mapping used failed on the outcome (for example because it is not a valid outcome), then the
``default`` value given to the constructor (which defaults to None) will be returned.
"""
def __init__(
self, mapping: OutcomeUtilityMapping, default=None, *args, **kwargs
) -> None:
super().__init__(*args, **kwargs)
self.mapping = mapping
self.default = default
[docs]
def to_dict(self, python_class_identifier=PYTHON_CLASS_IDENTIFIER):
d = {python_class_identifier: get_full_type_name(type(self))}
d.update(super().to_dict(python_class_identifier=python_class_identifier))
return dict(
**d,
mapping=serialize(
self.mapping, python_class_identifier=python_class_identifier
),
default=self.default,
)
[docs]
@classmethod
def from_dict(cls, d, python_class_identifier=PYTHON_CLASS_IDENTIFIER):
d.pop(python_class_identifier, None)
d["mapping"] = deserialize(
d["mapping"], python_class_identifier=python_class_identifier
)
return cls(**d)
[docs]
def eval(self, offer: Outcome | None) -> Distribution | float | None:
# noinspection PyBroadException
if offer is None:
return self.reserved_value
try:
m = gmap(self.mapping, offer)
except Exception:
return self.default
return m
[docs]
def xml(self, issues: list[Issue]) -> str:
raise NotImplementedError("Cannot save ProbMappingUtilityFunction to xml")
[docs]
@classmethod
def random(
cls,
outcome_space: OutcomeSpace,
reserved_value=(0.0, 1.0),
normalized=True,
max_cardinality: int = 10000,
type: str = "uniform",
):
# todo: corrrect this for continuous outcome-spaces
if not isinstance(reserved_value, Iterable):
reserved_value = (reserved_value, reserved_value)
os = outcome_space.to_largest_discrete(
levels=10, max_cardinality=max_cardinality
)
mn, rng = 0.0, 1.0
if not normalized:
mn = 4 * random.random()
rng = 4 * random.random()
locs = np.random.rand(os.cardinality) * rng + mn
scales = np.random.rand(os.cardinality) * rng + mn
return cls(
dict(
zip(
os.enumerate(),
(
ScipyDistribution(type=type, loc=k, scale=s)
for k, s in zip(locs, scales)
),
)
),
reserved_value=reserved_value[0] # type: ignore
+ random.random() * (reserved_value[1] - reserved_value[0]), # type: ignore
)
def __str__(self) -> str:
return f"mapping: {self.mapping}\ndefault: {self.default}"