from __future__ import annotations
import random
from abc import ABC, abstractmethod
from typing import Any, Generator
from negmas import warnings
from negmas.helpers import unique_name
from negmas.helpers.numeric import is_float_type, is_int_type, sample
from negmas.outcomes.base_issue import DiscreteIssue, Issue
__all__ = ["OrdinalIssue", "DiscreteOrdinalIssue"]
def generate_values(n: int) -> list[str]:
if n > 1000_000:
warnings.warn(
f"You are creating an OrdinalIssue with {n} items. This is too large."
f"Consider using something like ContiguousIssue if possible",
warnings.NegmasMemoryWarning,
)
width = len(str(n))
return list(f"{_:0{width}d}" for _ in range(n))
[docs]
class OrdinalIssue(Issue, ABC):
"""
An `Issue` that have some defined ordering of outcomes but not necessarily
a meaningful difference function between its values.
"""
[docs]
@abstractmethod
def ordered_value_generator( # type: ignore
self, n: int = 10, grid=True, compact=False, endpoints=True
) -> Generator[Any, None, None]:
...
[docs]
class DiscreteOrdinalIssue(DiscreteIssue, OrdinalIssue): # type: ignore
"""
A `DiscreteIssue` that have some defined ordering of outcomes but not necessarily a meaningful difference function between its values.
"""
def __init__(self, values, name=None) -> None:
"""
`values` can be an integer and in this case, values will be strings
"""
super().__init__(values, name)
if isinstance(values, int):
values = generate_values(values)
else:
values = list(values)
types = {type(_) for _ in values if _ is not None}
if len(types) == 1:
type_ = list(types)[0]
elif all(is_int_type(_) for _ in types):
type_ = int
elif all(is_float_type(_) for _ in types):
type_ = float
else:
raise TypeError(
f"Found the following types in the list of values for an "
f"ordinal issue ({types}). Can only have one type. Try "
f"CategoricalIssue"
)
self._value_type = type_
self._n_values = len(values)
self.min_value, self.max_value = min(values), max(values)
def _to_xml_str(self, indx):
output = f' <issue etype="discrete" index="{indx + 1}" name="{self.name}" type="discrete" vtype="discrete">\n'
for i, v in enumerate(self._values):
output += f' <item index="{i + 1}" value="{v}" cost="0" description="{v}">\n </item>\n'
output += " </issue>\n"
return output
@property
def all(self) -> Generator[Any, None, None]:
yield from self._values # type: ignore
[docs]
def rand_invalid(self): # type: ignore
"""Pick a random *invalid* value"""
if self.is_float():
return random.random() * self.max_value + self.max_value * 1.1
if self.is_integer():
return random.randint(self.max_value + 1, self.max_value * 2)
return unique_name("") + str(random.choice(self._values)) + unique_name("")
[docs]
def ordered_value_generator( # type: ignore
self, n: int = 10, grid=True, compact=False, endpoints=True
) -> Generator[Any, None, None]:
"""
A generator that generates at most `n` values (in order)
Remarks:
- This function returns a generator for the case when the number of values is very large.
- If you need a list then use something like:
>>> from negmas.outcomes import make_issue
>>> list(make_issue(5).ordered_value_generator())
[0, 1, 2, 3, 4]
>>> list(
... int(10 * _) for _ in make_issue((0.0, 1.0)).ordered_value_generator(11)
... )
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
"""
yield from (
self._values[_]
for _ in sample(
self.cardinality, n, grid=grid, compact=compact, endpoints=endpoints
)
)