From 32118bf58b39e0a97fae362ef8f7fb97f939e84f Mon Sep 17 00:00:00 2001 From: Yasser Mohammad Date: Fri, 22 Mar 2019 19:03:49 +0900 Subject: [PATCH] adding step time out --- negmas/apps/scml/world.py | 31 ++++++++++++++++++------ negmas/common.py | 8 ++++-- negmas/mechanisms.py | 13 +++++++++- negmas/sao.py | 17 ++++++++----- negmas/situated.py | 51 +++++++++++++++++++++++++++++++-------- 5 files changed, 94 insertions(+), 26 deletions(-) diff --git a/negmas/apps/scml/world.py b/negmas/apps/scml/world.py index 0c302661..7c4760cb 100644 --- a/negmas/apps/scml/world.py +++ b/negmas/apps/scml/world.py @@ -56,6 +56,7 @@ def __init__(self , time_limit=60 * 90 , neg_n_steps=100 , neg_time_limit=3 * 60 + , neg_step_time_limit=60 , negotiation_speed=10 # bank parameters , minimum_balance=0 @@ -137,6 +138,7 @@ def __init__(self , log_file_name=log_file_name, awi_type='negmas.apps.scml.SCMLAWI' , default_signing_delay=default_signing_delay , start_negotiations_immediately=start_negotiations_immediately + , neg_step_time_limit=neg_step_time_limit , name=name) if balance_at_max_interest is None: balance_at_max_interest = initial_wallet_balances @@ -166,6 +168,8 @@ def __init__(self self.bulletin_board.record(section='settings', key='negotiation_speed_multiple' , value=negotiation_speed) self.bulletin_board.record(section='settings', key='negotiation_n_steps', value=neg_n_steps) + self.bulletin_board.record(section='settings', key='negotiation_step_time_limit', value=neg_step_time_limit) + self.bulletin_board.record(section='settings', key='negotiation_time_limit', value=neg_time_limit) self.bulletin_board.record(section='settings', key='transportation_delay', value=transportation_delay) self.avg_process_cost_is_public = avg_process_cost_is_public self.catalog_prices_are_public = catalog_prices_are_public @@ -934,8 +938,8 @@ def _execute_contract(self, contract: Contract) -> Set[Breach]: if not partner.confirm_contract_execution(contract=contract): self.logdebug( f'{partner.name} refused execution og Contract {contract.id}') - breaches.add(Breach(contract=contract, perpetrator=partner # type: ignore - , victims=list(partners - {partner}) + breaches.add(Breach(contract=contract, perpetrator=partner.id # type: ignore + , victims=[_.id for _ in list(partners - {partner})] , level=1.0, type='refusal')) if len(breaches) > 0: return breaches @@ -1032,10 +1036,10 @@ def _execute_contract(self, contract: Contract) -> Set[Breach]: # pay penalties if there are any. Notice that penalties apply only to to seller. It makes no sense to have a # penalty on the buyer who already have no money to pay the contract value anyway. if penalty_breach_society is not None: - breaches.add(Breach(contract=contract, perpetrator=seller, victims=[] + breaches.add(Breach(contract=contract, perpetrator=seller.id, victims=[] , level=penalty_breach_society, type='penalty_society', step=self.current_step)) if penalty_breach_victim is not None: - breaches.add(Breach(contract=contract, perpetrator=seller, victims=[buyer] + breaches.add(Breach(contract=contract, perpetrator=seller.id, victims=[buyer.id] , level=penalty_breach_victim, type='penalty_society', step=self.current_step)) # check the buyer @@ -1057,7 +1061,7 @@ def _execute_contract(self, contract: Contract) -> Set[Breach]: if product_breach is not None: # apply insurances if they exist # register the breach independent of insurance - breaches.add(Breach(contract=contract, perpetrator=seller, victims=[buyer] + breaches.add(Breach(contract=contract, perpetrator=seller.id, victims=[buyer.id] , level=product_breach, type='product', step=self.current_step)) if self.insurance_company.pay_insurance(contract=contract, perpetrator=seller): # if the buyer has an insurance against the seller for this contract, then just give him the missing @@ -1088,7 +1092,7 @@ def _execute_contract(self, contract: Contract) -> Set[Breach]: if money_breach is not None: # apply insurances if they exist. - breaches.add(Breach(contract=contract, perpetrator=buyer, victims=[seller] + breaches.add(Breach(contract=contract, perpetrator=buyer.id, victims=[seller.id] , level=money_breach, type='money', step=self.current_step)) if self.insurance_company.pay_insurance(contract=contract, perpetrator=buyer): # if the seller has an insurance against the buyer for this contract, then just give him the missing @@ -1132,7 +1136,17 @@ def _execute_contract(self, contract: Contract) -> Set[Breach]: # quantity = int(math.floor(money / unit_price)) if money > 0 or quantity > 0: - self._move_product(buyer=buyer, seller=seller, quantity=quantity, money=money, product_id=pind) + perpetrators = set([b.perpetrator for b in breaches]) + execute = True + victims = {} + if 0 < len(perpetrators) < len(partners): + victims = set(partners) - set(perpetrators) + execute = all(victim.confirm_partial_execution(contract=contract, breaches=list(breaches)) + for victim in victims) + if execute: + self._move_product(buyer=buyer, seller=seller, quantity=quantity, money=money, product_id=pind) + else: + self.logdebug(f'Contract {contract.id}: one of {[_.id for _ in victims]} refused partial execution.') else: self.logdebug(f'Contract {contract.id} has no transfers') return breaches @@ -1156,6 +1170,9 @@ def _move_product(self, buyer: SCMLAgent, seller: SCMLAgent, product_id: int, qu self.logdebug(f'Moved {quantity} units of {self.products[product_id].name} from {seller.name} to {buyer.name} ' f'for {money} dollars') + def _complete_contract_execution(self, contract: Contract, breaches: List[Breach], resolved: bool): + pass + def _move_product_force(self, buyer: SCMLAgent, seller: SCMLAgent, product_id: int, quantity: int, money: float): """Moves as much product and money between the buyer and seller""" seller_factory, buyer_factory = self.a2f[seller.id], self.a2f[buyer.id] diff --git a/negmas/common.py b/negmas/common.py index 23ea646f..d68195ee 100644 --- a/negmas/common.py +++ b/negmas/common.py @@ -64,6 +64,8 @@ class MechanismState: """Does the mechanism have any errors""" error_details: str = '' """Details of the error if any""" + info: 'MechanismInfo' = None + """Mechanism information""" @property def ended(self): @@ -103,7 +105,7 @@ def asdict(self): return {_.name: self.__dict__[_.name] for _ in fields(self)} class Java: - implements = ['jnegmas.ProductionFailure'] + implements = ['jnegmas.common.MechanismState'] @dataclass @@ -119,6 +121,8 @@ class MechanismInfo: """A lit of *all possible* outcomes for a negotiation. None if the number of outcomes is uncountable""" time_limit: float """The time limit in seconds for this negotiation session. None indicates infinity""" + step_time_limit: float + """The time limit in seconds for each step of this negotiation session. None indicates infinity""" n_steps: int """The allowed number of steps for this negotiation. None indicates infinity""" dynamic_entry: bool @@ -242,7 +246,7 @@ def asdict(self): return {_.name: self.__dict__[_.name] for _ in fields(self)} class Java: - implements = ['jnegmas.ProductionFailure'] + implements = ['jnegmas.common.PyMechanismInfo'] def register_all_mechanisms(mechanisms: typing.Dict[str, 'Mechanism']) -> None: diff --git a/negmas/mechanisms.py b/negmas/mechanisms.py index 68ed5a87..a8e92b86 100644 --- a/negmas/mechanisms.py +++ b/negmas/mechanisms.py @@ -62,6 +62,7 @@ def __init__( outcomes: Union[int, List['Outcome']] = None, n_steps: int = None, time_limit: float = None, + step_time_limit: float = None, max_n_agents: int = None, dynamic_entry=False, cache_outcomes=True, @@ -160,6 +161,7 @@ def __init__( , outcomes=__outcomes , time_limit=time_limit , n_steps=n_steps + , step_time_limit = step_time_limit , dynamic_entry=dynamic_entry , max_n_agents=max_n_agents , annotation=annotation @@ -571,6 +573,10 @@ def step(self) -> MechanismState: - There is another function (`run()`) that runs the whole mechanism in blocking mode """ + if self.time > self.time_limit: + self._agreement, self._broken, self._timedout = None, False, True + self._history.append(self.state) + return self.state if len(self._agents) < 2: if self.info.dynamic_entry: self._history.append(self.state) @@ -611,11 +617,16 @@ def step(self) -> MechanismState: for agent in self._agents: agent.on_round_start(state=self.state) + step_start = time.perf_counter() result = self.step_() + step_time = time.perf_counter() - step_start self._error, self._error_details = result.error, result.error_details if self._error: self.on_mechanism_error() - self._broken, self._timedout, self._agreement = result.broken, result.timedout, result.agreement + if step_time > self.info.step_time_limit: + self._broken, self._timedout, self._agreement = False, True, None + else: + self._broken, self._timedout, self._agreement = result.broken, result.timedout, result.agreement if (self._agreement is not None) or self._broken or self._timedout: self._running = False self._step += 1 diff --git a/negmas/sao.py b/negmas/sao.py index 1c55a94d..a5767d64 100644 --- a/negmas/sao.py +++ b/negmas/sao.py @@ -8,7 +8,7 @@ from negmas.common import * from negmas.events import Notification -from negmas.java import JNegmasGateway, JavaCallerMixin, to_java +from negmas.java import JNegmasGateway, JavaCallerMixin, to_dict from negmas.mechanisms import MechanismRoundResult, Mechanism from negmas.negotiators import Negotiator, AspirationMixin, Controller from negmas.outcomes import sample_outcomes, Outcome, outcome_is_valid, ResponseType, outcome_as_dict @@ -63,6 +63,7 @@ def __init__( outcomes=None, n_steps=None, time_limit=None, + step_time_limit=None, max_n_agents=None, dynamic_entry=True, keep_issue_names=True, @@ -77,6 +78,7 @@ def __init__( outcomes=outcomes, n_steps=n_steps, time_limit=time_limit, + step_time_limit=step_time_limit, max_n_agents=max_n_agents, dynamic_entry=dynamic_entry, keep_issue_names=keep_issue_names, @@ -600,6 +602,9 @@ def propose_(self, state: MechanismState) -> Optional['Outcome']: """ + class Java: + implements = ['jnegmas.sao.PySAONegotiator'] + class RandomNegotiator(Negotiator, RandomResponseMixin, RandomProposalMixin): """A negotiation agent that responds randomly in a single negotiation.""" @@ -1023,15 +1028,15 @@ def __init__(self, java_class_name: Optional[str] ) if java_class_name is not None: self.init_java_bridge(java_class_name=java_class_name, auto_load_java=auto_load_java) - self.java_object.fromMap(to_java(self)) + self.java_object.fromMap(to_dict(self)) @classmethod - def from_java(cls, java_object, *args, parent: Controller = None) -> 'JavaSAONegotiator': + def from_dict(cls, java_object, *args, parent: Controller = None) -> 'JavaSAONegotiator': """Creates a Java negotiator from an object returned from the JVM implementing PySAONegotiator""" ufun = java_object.getUtilityFunction() if ufun is not None: - ufun = JavaUtilityFunction.from_java(java_object=ufun) - return JavaCallerMixin.from_java(java_object, name=java_object.getName() + ufun = JavaUtilityFunction.from_dict(java_object=ufun) + return JavaCallerMixin.from_dict(java_object, name=java_object.getName() , assume_normalized=java_object.getAssumeNormalized() , rational_proposal=java_object.getRationalProposal() , parent=parent @@ -1039,7 +1044,7 @@ def from_java(cls, java_object, *args, parent: Controller = None) -> 'JavaSAONeg def on_notification(self, notification: Notification, notifier: str): super().on_notification(notification=notification, notifier=notifier) - jnotification = {'type': notification.type, 'data': to_java(notification.data)} + jnotification = {'type': notification.type, 'data': to_dict(notification.data)} self.java_object.on_notification(jnotification, notifier) def respond_(self, state: MechanismState, offer: 'Outcome'): diff --git a/negmas/situated.py b/negmas/situated.py index 835ae386..45ad967f 100644 --- a/negmas/situated.py +++ b/negmas/situated.py @@ -123,18 +123,18 @@ def __hash__(self): return self.id.__hash__() class Java: - implements = ['jnegmas.Contract'] + implements = ['jnegmas.situated.Contract'] @dataclass class Breach: contract: Contract """The agreement being breached""" - perpetrator: 'Agent' - """The agent committing the breach""" + perpetrator: str + """ID of the agent committing the breach""" type: str """The type of the breach. Can be one of: `refusal`, `product`, `money`, `penalty`.""" - victims: List['Agent'] = field(default_factory=list) + victims: List[str] = field(default_factory=list) """Specific victims of the breach. If not given all partners in the agreement (except perpetrator) are considered victims""" level: float = 1.0 @@ -160,12 +160,17 @@ def as_dict(self): 'id': self.id, 'perpetrator': self.perpetrator.id, 'perpetrator_type': self.perpetrator.__class__.__name__, - 'victims': [_.id for _ in self.victims], - 'victim_types': [_.__class__.__name__ for _ in self.victims], + 'victims': [_ for _ in self.victims], 'step': self.step, 'resolved': None, } + def to_dict(self): + return self.as_dict() + + class Java: + implements = ['jnegmas.situated.Breach'] + class BreachProcessing(Enum): """The way breaches are to be handled""" @@ -417,6 +422,7 @@ def __init__(self, world: 'World', mechanism_name: str, mechanism_params: Dict[s , issues: List['Issue'], req_id: str , caller: 'Agent', partners: List['Agent'], roles: Optional[List[str]] = None , annotation: Dict[str, Any] = None, neg_n_steps: int = None, neg_time_limit: int = None + , neg_step_time_limit = None ): self.mechanism_name, self.mechanism_params = mechanism_name, mechanism_params self.caller = caller @@ -425,6 +431,7 @@ def __init__(self, world: 'World', mechanism_name: str, mechanism_params: Dict[s self.annotation = annotation self.neg_n_steps = neg_n_steps self.neg_time_limit = neg_time_limit + self.neg_step_time_limit = neg_step_time_limit self.world = world self.req_id = req_id self.issues = issues @@ -437,6 +444,8 @@ def _create_negotiation_session(self, mechanism: MechanismProxy mechanism.info.n_steps = self.neg_n_steps if self.neg_time_limit is not None: mechanism.info.time_limit = self.neg_time_limit + if self.neg_step_time_limit is not None: + mechanism.info.step_time_limit = self.neg_step_time_limit for partner in partners: mechanism.register_listener(event_type='negotiation_end', listener=partner) for _negotiator, _role in responses: @@ -463,6 +472,7 @@ def _start_negotiation(self, mechanism_name, mechanism_params, roles, caller, pa mechanism_params = {k: v for k, v in mechanism_params.items() if k != PROTOCOL_CLASS_NAME_FIELD} mechanism_params['n_steps'] = self.neg_n_steps mechanism_params['time_limit'] = self.neg_time_limit + mechanism_params['step_time_limit'] = self.neg_step_time_limit mechanism_params['issues'] = issues mechanism_params['annotation'] = annotation mechanism_params['name'] = '-'.join(_.id for _ in partners) @@ -743,6 +753,7 @@ def __init__(self, bulletin_board: BulletinBoard = None , negotiation_speed=None , neg_n_steps=100 , neg_time_limit=3 * 60 + , neg_step_time_limit=60 , default_signing_delay=0 , breach_processing=BreachProcessing.VICTIM_THEN_PERPETRATOR , log_file_name='' @@ -765,6 +776,7 @@ def __init__(self, bulletin_board: BulletinBoard = None time_limit: Real-time limit on the simulation negotiation_speed: The number of negotiation steps per simulation step. None means infinite neg_n_steps: Maximum number of steps allowed for a negotiation. + neg_step_time_limit: Time limit for single step of the negotiation protocol. neg_time_limit: Real-time limit on each single negotiation name: Name of the simulator """ @@ -788,6 +800,7 @@ def __init__(self, bulletin_board: BulletinBoard = None self.time_limit = time_limit self.neg_n_steps = neg_n_steps self.neg_time_limit = neg_time_limit + self.neg_step_time_limit = neg_step_time_limit self._entities: Dict[int, Set[ActiveEntity]] = defaultdict(set) self._negotiations: Dict[str, NegotiationInfo] = {} self._start_time = -1 @@ -990,7 +1003,9 @@ def _run_negotiations(n_steps: Optional[int] = None): self._saved_breaches[b.id] = b.as_dict() current_contracts = [_[0] for _ in breached_contracts] for contract, contract_breaches in breached_contracts: - n_new_breaches += 1 - int(self._process_breach(contract, list(contract_breaches))) + resolved = self._process_breach(contract, list(contract_breaches)) + n_new_breaches += 1 - int(resolved) + self._complete_contract_execution(contract, contract_breaches, resolved) self._delete_executed_contracts() # note that all contracts even breached ones are to be deleted @@ -1091,7 +1106,9 @@ def _register_negotiation(self, mechanism_name, mechanism_params, roles, caller, factory = MechanismFactory(world=self, mechanism_name=mechanism_name, mechanism_params=mechanism_params , issues=issues, req_id=req_id, caller=caller, partners=partners , roles=roles, annotation=annotation - , neg_n_steps=self.neg_n_steps, neg_time_limit=self.neg_time_limit) + , neg_n_steps=self.neg_n_steps + , neg_time_limit=self.neg_time_limit + , neg_step_time_limit=self.neg_step_time_limit) neg = factory.init() if neg is None: return None @@ -1389,7 +1406,7 @@ def _execute_contract(self, contract: Contract) -> Set[Breach]: contract: Returns: - Set[Breach]: The set of breaches sommitted if any. If there are no breaches return an empty set + Set[Breach]: The set of breaches committed if any. If there are no breaches return an empty set Remarks: @@ -1399,6 +1416,20 @@ def _execute_contract(self, contract: Contract) -> Set[Breach]: self.loginfo(f'Executing {str(contract)}') return set() + @abstractmethod + def _complete_contract_execution(self, contract: Contract, breaches: List[Breach], resolved: bool) -> None: + """ + Called after breach resolution is completed for contracts for which some potential breaches occurred. + + Args: + contract: The contract considered. + breaches: The list of potential breaches that was generated by `_execute_contract`. + resolved: Whether the breaches were resolved. + + Returns: + + """ + def _process_breach(self, contract: Contract, breaches: List[Breach] , force_immediate_signing=True) -> bool: resolved = False @@ -1406,7 +1437,7 @@ def _process_breach(self, contract: Contract, breaches: List[Breach] # calculate total breach level total_breach_levels = defaultdict(int) for breach in breaches: - total_breach_levels[breach.perpetrator.id] += breach.level + total_breach_levels[breach.perpetrator] += breach.level # give agents the chance to set renegotiation agenda in ascending order of their total breach levels for agent_name, _ in sorted(zip(total_breach_levels.keys(), total_breach_levels.values()), key=lambda x: x[1]):