Skip to content

Commit

Permalink
Using python-class-identifier everywhere
Browse files Browse the repository at this point in the history
This paramter to loading/saving functions allows the caller to change
the identifier used to mark python classes in dumped files.
  • Loading branch information
yasserfarouk committed Dec 25, 2024
1 parent c81a663 commit ca20bbc
Show file tree
Hide file tree
Showing 31 changed files with 447 additions and 168 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ dependencies = [
"pytest-runner>=6.0.1",
"pandas>=2.2.3",
"scipy>=1.14.1",
"numpy>=2.1.3",
"numpy>=2.0.0",
]

[build-system]
Expand Down
16 changes: 13 additions & 3 deletions src/negmas/events.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Implements Event management"""

from __future__ import annotations
import json
import random
Expand All @@ -11,7 +12,7 @@
from negmas import warnings

from .outcomes import Issue
from .serialization import serialize
from .serialization import PYTHON_CLASS_IDENTIFIER, serialize
from .types import NamedObject

__all__ = [
Expand Down Expand Up @@ -98,7 +99,12 @@ def __init__(self, file_name: str | Path, types: list[str] | None = None):
def reset_timer(self):
self._start = time.perf_counter()

def on_event(self, event: Event, sender: EventSource):
def on_event(
self,
event: Event,
sender: EventSource,
python_class_identifier=PYTHON_CLASS_IDENTIFIER,
):
if not self._file_name:
return
if self._types is not None and event.type not in self._types:
Expand All @@ -122,7 +128,11 @@ def _simplify(x):
# return _simplify(myvars(x))

try:
sid = sender.id if hasattr(sender, "id") else serialize(sender) # type: ignore
sid = (
sender.id
if hasattr(sender, "id")
else serialize(sender, python_class_identifier=python_class_identifier)
) # type: ignore
d = dict(
sender=sid,
time=time.perf_counter() - self._start,
Expand Down
6 changes: 5 additions & 1 deletion src/negmas/gb/negotiators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
TState = TypeVar("TState", bound=MechanismState)


def none_return():
return None


class GBNegotiator(Negotiator[GBNMI, GBState], Generic[TNMI, TState]):
"""
Base class for all GB negotiators.
Expand Down Expand Up @@ -64,7 +68,7 @@ def __init__(
self.add_capabilities({"respond": True, "propose": True, "max-proposals": 1})
self.__end_negotiation = False
self.__received_offer: dict[str | None, Outcome | None] = defaultdict(
lambda: None
none_return
)

@abstractmethod
Expand Down
2 changes: 1 addition & 1 deletion src/negmas/gb/negotiators/utilbased.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def utility_range_to_propose(self, state) -> tuple[float, float]:
def utility_range_to_accept(self, state) -> tuple[float, float]:
...

def respond(self, state, source: str | None = None):
def respond(self, state, source: str | None = None) -> ResponseType:
offer = state.current_offer # type: ignore
if self._selector:
self._selector.before_responding(state, offer, source)
Expand Down
7 changes: 5 additions & 2 deletions src/negmas/genius/negotiator.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,11 @@ def on_negotiation_start(self, state: MechanismState) -> None:
)
)
n_rounds = info.n_steps
if n_rounds is not None and info.one_offer_per_step:
n_rounds = int(math.ceil(float(n_rounds) / info.n_negotiators))
try:
if n_rounds is not None and info.one_offer_per_step:
n_rounds = int(math.ceil(float(n_rounds) / info.n_negotiators))
except Exception:
pass
n_steps = -1 if n_rounds is None or math.isinf(n_rounds) else int(n_rounds)
n_seconds = (
-1
Expand Down
36 changes: 29 additions & 7 deletions src/negmas/inout.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,9 @@ def calc_extra_stats(
bilateral_frontier_outcomes=fo,
)

def serialize(self) -> dict[str, Any]:
def serialize(
self, python_class_identifier=PYTHON_CLASS_IDENTIFIER
) -> dict[str, Any]:
"""
Converts the current scenario into a serializable dict.
Expand Down Expand Up @@ -507,11 +509,24 @@ def adjust(
return d

domain = adjust(
serialize(self.outcome_space, shorten_type_field=True, add_type_field=True),
serialize(
self.outcome_space,
shorten_type_field=True,
add_type_field=True,
python_class_identifier=python_class_identifier,
),
"domain",
)
ufuns = [
adjust(serialize(u, shorten_type_field=True, add_type_field=True), i)
adjust(
serialize(
u,
shorten_type_field=True,
add_type_field=True,
python_class_identifier=python_class_identifier,
),
i,
)
for i, u in enumerate(self.ufuns)
]
return dict(domain=domain, ufuns=ufuns)
Expand All @@ -532,13 +547,19 @@ def to_json(self, folder: Path | str) -> None:
"""
self.dumpas(folder, "json")

def dumpas(self, folder: Path | str, type="yml", compact: bool = False) -> None:
def dumpas(
self,
folder: Path | str,
type="yml",
compact: bool = False,
python_class_identifier=PYTHON_CLASS_IDENTIFIER,
) -> None:
"""
Dumps the scenario in the given file format.
"""
folder = Path(folder)
folder.mkdir(parents=True, exist_ok=True)
serialized = self.serialize()
serialized = self.serialize(python_class_identifier=python_class_identifier)
dump(serialized["domain"], folder / f"{serialized['domain']['name']}.{type}")
for u in serialized["ufuns"]:
dump(u, folder / f"{u['name']}.{type}", sort_keys=True, compact=compact)
Expand Down Expand Up @@ -666,6 +687,7 @@ def from_yaml_files(
ignore_discount=False,
ignore_reserved=False,
safe_parsing=True,
python_class_identifier="type",
) -> Scenario | None:
_ = safe_parsing # yaml parsing is always safe

Expand All @@ -677,13 +699,13 @@ def adjust_type(d: dict, base: str = "negmas", domain=None) -> dict:

os = deserialize(
adjust_type(load(domain)),
python_class_identifier="type",
base_module="negmas",
python_class_identifier=python_class_identifier,
)
utils = tuple(
deserialize(
adjust_type(load(fname), domain=os),
python_class_identifier="type",
python_class_identifier=python_class_identifier,
base_module="negmas",
)
for fname in ufuns
Expand Down
2 changes: 1 addition & 1 deletion src/negmas/mechanisms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1355,7 +1355,7 @@ def runall(
raise NotImplementedError()
else:
raise ValueError(
f"method {method} is unknown. Acceptable options are serial, threads, processes"
f"method {method} is unknown. Acceptable options are ordered, sequential, threads, processes"
)
return states

Expand Down
19 changes: 13 additions & 6 deletions src/negmas/outcomes/base_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,23 +261,30 @@ def rand_valid(self):
return self.rand()

@classmethod
def from_dict(cls, d):
def from_dict(cls, d, python_class_identifier=PYTHON_CLASS_IDENTIFIER):
"""
Constructs an issue from a dict generated using `to_dict()`
"""
if isinstance(d, cls):
return d
d.pop(PYTHON_CLASS_IDENTIFIER, None)
d["values"] = deserialize(d["values"])
d.pop(python_class_identifier, None)
d["values"] = deserialize(
d["values"], python_class_identifier=python_class_identifier
)
return cls(values=d.get("values", None), name=d.get("name", None))

def to_dict(self):
def to_dict(self, python_class_identifier=PYTHON_CLASS_IDENTIFIER):
"""
Converts the issue to a dictionary from which it can be constructed again using `Issue.from_dict()`
"""
d = {PYTHON_CLASS_IDENTIFIER: get_full_type_name(type(self))}
d = {python_class_identifier: get_full_type_name(type(self))}
return dict(
**d, values=serialize(self.values), name=self.name, n_values=self._n_values
**d,
values=serialize(
self.values, python_class_identifier=python_class_identifier
),
name=self.name,
n_values=self._n_values,
)

@abstractmethod
Expand Down
5 changes: 3 additions & 2 deletions src/negmas/outcomes/continuous_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from negmas.outcomes.base_issue import Issue
from negmas.outcomes.range_issue import RangeIssue
from negmas.serialization import PYTHON_CLASS_IDENTIFIER

from .common import DEFAULT_LEVELS

Expand All @@ -30,8 +31,8 @@ def _to_xml_str(self, indx):
f' <range lowerbound="{self._values[0]}" upperbound="{self._values[1]}"></range>\n </issue>\n'
)

def to_dict(self):
d = super().to_dict()
def to_dict(self, python_class_identifier=PYTHON_CLASS_IDENTIFIER):
d = super().to_dict(python_class_identifier=python_class_identifier)
d["n_levels"] = self._n_levels
return d

Expand Down
14 changes: 8 additions & 6 deletions src/negmas/outcomes/optional_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,24 +75,26 @@ def rand(self) -> int | float | str | None:
return self.base.rand()

@classmethod
def from_dict(cls, d):
def from_dict(cls, d, python_class_identifier=PYTHON_CLASS_IDENTIFIER):
"""
Constructs an issue from a dict generated using `to_dict()`
"""
if isinstance(d, cls):
return d
d.pop(PYTHON_CLASS_IDENTIFIER, None)
d["base"] = deserialize(d["base"])
d.pop(python_class_identifier, None)
d["base"] = deserialize(
d["base"], python_class_identifier=python_class_identifier
)
return cls(base=d.get("base", None), name=d.get("name", None))

def to_dict(self):
def to_dict(self, python_class_identifier=PYTHON_CLASS_IDENTIFIER):
"""
Converts the issue to a dictionary from which it can be constructed again using `Issue.from_dict()`
"""
d = {PYTHON_CLASS_IDENTIFIER: get_full_type_name(type(self))}
d = {python_class_identifier: get_full_type_name(type(self))}
return dict(
**d,
base=serialize(self.base),
base=serialize(self.base, python_class_identifier=python_class_identifier),
name=self.name,
n_values=self.cardinality + 1,
)
Expand Down
23 changes: 18 additions & 5 deletions src/negmas/outcomes/outcome_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ def update(self):
class EnumeratingOutcomeSpace(DiscreteOutcomeSpace, OSWithValidity):
"""An outcome space representing the enumeration of some outcomes. No issues defined"""

name: str | None = field(eq=False, default=None)

def invalidate(self, outcome: Outcome) -> None:
"""Indicates that the outcome is invalid"""
self.invalid.add(outcome)
Expand Down Expand Up @@ -266,6 +268,11 @@ def __attrs_post_init__(self):
if not self.name:
object.__setattr__(self, "name", unique_name("os", add_time=False, sep=""))

def __mul__(self, other: CartesianOutcomeSpace) -> CartesianOutcomeSpace:
issues = list(self.issues) + list(other.issues)
name = f"{self.name}*{other.name}"
return CartesianOutcomeSpace(tuple(issues), name=name)

def contains_issue(self, x: Issue) -> bool:
"""Cheks that the given issue is in the tuple of issues constituting the outcome space (i.e. it is one of its dimensions)"""
return x in self.issues
Expand Down Expand Up @@ -299,13 +306,19 @@ def contains_os(self, x: OutcomeSpace) -> bool:
)
return all(self.is_valid(_) for _ in x.enumerate()) # type: ignore If we are here, we know that x is finite

def to_dict(self):
d = {PYTHON_CLASS_IDENTIFIER: get_full_type_name(type(self))}
return dict(**d, name=self.name, issues=serialize(self.issues))
def to_dict(self, python_class_identifier=PYTHON_CLASS_IDENTIFIER):
d = {python_class_identifier: get_full_type_name(type(self))}
return dict(
**d,
name=self.name,
issues=serialize(
self.issues, python_class_identifier=python_class_identifier
),
)

@classmethod
def from_dict(cls, d):
return cls(**deserialize(d)) # type: ignore
def from_dict(cls, d, python_class_identifier=PYTHON_CLASS_IDENTIFIER):
return cls(**deserialize(d, python_class_identifier=python_class_identifier)) # type: ignore

@property
def issue_names(self) -> list[str]:
Expand Down
1 change: 1 addition & 0 deletions src/negmas/outcomes/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
runtime_checkable,
)


if TYPE_CHECKING:
from .base_issue import DiscreteIssue, Issue
from .common import Outcome
Expand Down
25 changes: 16 additions & 9 deletions src/negmas/preferences/base_ufun.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def __init__(
self.reserved_value = reserved_value
self._cached_inverse: InverseUFun | None = None
self._cached_inverse_type: type[InverseUFun] | None = None
self.__invalid_value = invalid_value
self._invalid_value = invalid_value

@abstractmethod
def eval(self, offer: Outcome) -> Value:
Expand Down Expand Up @@ -465,20 +465,27 @@ def to_prob(self) -> ProbUtilityFunction:

return ProbAdapter(self)

def to_dict(self) -> dict[str, Any]:
d = {PYTHON_CLASS_IDENTIFIER: get_full_type_name(type(self))}
def to_dict(
self, python_class_identifier=PYTHON_CLASS_IDENTIFIER
) -> dict[str, Any]:
d = {python_class_identifier: get_full_type_name(type(self))}
return dict(
**d,
outcome_space=serialize(self.outcome_space),
outcome_space=serialize(
self.outcome_space, python_class_identifier=python_class_identifier
),
reserved_value=self.reserved_value,
name=self.name,
id=self.id,
)

@classmethod
def from_dict(cls, d):
d.pop(PYTHON_CLASS_IDENTIFIER, None)
d["outcome_space"] = deserialize(d.get("outcome_space", None))
def from_dict(cls, d, python_class_identifier=PYTHON_CLASS_IDENTIFIER):
d.pop(python_class_identifier, None)
d["outcome_space"] = deserialize(
d.get("outcome_space", None),
python_class_identifier=python_class_identifier,
)
return cls(**d)

def sample_outcome_with_utility(
Expand Down Expand Up @@ -1118,11 +1125,11 @@ def __call__(self, offer: Outcome | None) -> Value:
if offer is None:
return self.reserved_value # type: ignore I know that concrete subclasses will be returning the correct type
if (
self.__invalid_value is not None
self._invalid_value is not None
and self.outcome_space
and offer not in self.outcome_space
):
return self.__invalid_value
return self._invalid_value
return self.eval(offer)


Expand Down
Loading

0 comments on commit ca20bbc

Please sign in to comment.