from __future__ import annotations
import math
import numbers
import random
from typing import Generator, Iterable
import numpy as np
from negmas.helpers.numeric import sample
from negmas.outcomes.base_issue import DiscreteIssue, Issue
from negmas.outcomes.range_issue import RangeIssue
__all__ = ["ContiguousIssue"]
[docs]
class ContiguousIssue(RangeIssue, DiscreteIssue):
"""
A `RangeIssue` (also a `DiscreteIssue`) representing a contiguous range of integers.
"""
def __init__(self, values: int | tuple[int, int], name: str | None = None) -> None:
vt: tuple[int, int]
vt = values # type: ignore
if isinstance(vt, numbers.Integral):
vt = (0, int(vt) - 1)
if isinstance(vt, Iterable):
vt = tuple(vt)
if len(vt) != 2:
raise ValueError(
f"{self.__class__.__name__} should receive one or two values for "
f"the minimum and maximum limits but received {values=}"
)
if not isinstance(vt[0], numbers.Integral) or not isinstance(
vt[1], numbers.Integral
):
raise ValueError(
f"{self.__class__.__name__} should receive one or two integers for"
f" the minimum and maximum limits but received {values=}"
)
vt = tuple(vt)
super().__init__(vt, name)
self._n_values = vt[1] - vt[0] + 1 # type: ignore
def _to_xml_str(self, indx):
# return (
# f' <issue etype="integer" index="{indx + 1}" name="{self.name}" type="integer" vtype="integer"'
# f' lowerbound="{self._values[0]}" upperbound="{self._values[1]}" />\n'
# )
output = f' <issue etype="discrete" index="{indx + 1}" name="{self.name}" type="discrete" vtype="integer">\n'
for i, v in enumerate(range(self._values[0], self._values[1] + 1)):
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[int, None, None]:
yield from range(self._values[0], self._values[1] + 1)
@property
def cardinality(self) -> int:
return self.max_value - self.min_value + 1
[docs]
def ordered_value_generator(
self, n: int | float | None = None, grid=True, compact=False, endpoints=True
) -> Generator[int, None, None]:
m = self.cardinality
n = m if n is None or not math.isfinite(n) else int(n)
for i in range(n):
yield self._values[0] + (i % m)
[docs]
def value_generator(
self, n: int | float | None = 10, grid=True, compact=False, endpoints=True
) -> Generator[int, None, None]:
yield from (
_ + self._values[0]
for _ in sample(
self.cardinality, n, grid=grid, compact=compact, endpoints=endpoints
)
)
[docs]
def to_discrete(
self, n: int | None, grid=True, compact=False, endpoints=True
) -> DiscreteIssue:
if n is None or self.cardinality < n:
return self
if not compact:
return super().to_discrete(
n, grid=grid, compact=compact, endpoints=endpoints
)
beg = (self.cardinality - n) // 2
return ContiguousIssue((int(beg), int(beg + n)), name=self.name + f"{n}")
[docs]
def rand(self) -> int:
"""Picks a random valid value."""
return random.randint(*self._values)
[docs]
def rand_outcomes(
self, n: int, with_replacement=False, fail_if_not_enough=False
) -> list[int]:
"""Picks a random valid value."""
if n > self._n_values and not with_replacement:
if fail_if_not_enough:
raise ValueError(
f"Cannot sample {n} outcomes out of {self._values} without replacement"
)
return [_ for _ in range(self._values[0], self._values[1] + 1)]
if with_replacement:
return np.random.randint(
low=self._values[0], high=self._values[1] + 1, size=n
).tolist()
vals = [_ for _ in range(self._values[0], self._values[1] + 1)]
random.shuffle(vals)
return vals[:n]
[docs]
def rand_invalid(self):
"""Pick a random *invalid* value"""
return random.randint(self.max_value + 1, 2 * self.max_value)
[docs]
def is_continuous(self) -> bool:
return False
[docs]
def value_at(self, index: int):
if index < 0 or index > self.cardinality - 1:
raise IndexError(index)
return self.min_value + index
[docs]
def contains(self, issue: Issue) -> bool:
"""Checks weather this issue contains the input issue (i.e. every value in the input issue is in this issue)"""
return (
issubclass(issue.value_type, numbers.Integral)
and issue.min_value >= self.min_value
and issue.max_value <= self.max_value
)