"""
This module implements a factory manager for the SCM league of ANAC 2019 competition. This advanced version has all
callbacks. Please refer to the [http://www.yasserm.com/scml/scml.pdf](game description)
for all the callbacks.
Your agent can learn about the state of the world and itself by accessing properties in the AWI it has. For example::
self.awi.n_steps # gives the number of simulation steps
You can access the state of your factory as::
self.awi.state
Your agent can act in the world by calling methods in the AWI it has. For example: >>> self.awi.register_cfp(cfp) # registers a new CFP
You can access the full list of these capabilities on the documentation.
- For properties/methods available only to SCM agents, check the list here:
https://negmas.readthedocs.io/en/latest/api/scml.scml2019.SCMLAWI.html
- For properties/methods available to all kinds of agents in all kinds of worlds, check the list here:
https://negmas.readthedocs.io/en/latest/api/negmas.situated.AgentWorldInterface.html
The SCMLAgent class itself has some helper properties/methods that internally call the AWI. These include:
- request_negotiation(): Generates a unique identifier for this negotiation request and passes it to the AWI through a
call of awi.request_negotiation(). It is recommended to use this method always to request
negotiations. This way, you can access internal lists of requested_negotiations, and
running_negotiations. If on the other hand you use awi.request_negotiation(), these internal
lists will not be updated and you will have to keep track to requested and running negotiations
manually if you need to use them.
- can_expect_agreement(): Checks if it is possible in principle to get an agreement on this CFP by the time it becomes
executable
- products, processes: shortcuts to awi.products and awi.processes
"""
import itertools
import math
from collections import defaultdict
from operator import attrgetter
from negmas import Contract, Breach, RenegotiationRequest, Negotiator, AgentMechanismInterface
from negmas import MechanismState
from negmas.events import Notification
from negmas.helpers import get_class
from negmas.sao import AspirationNegotiator
from negmas.utilities import normalize
from typing import Dict, Any, Callable, Collection, Type, List, Optional, Union
from scml.scml2019.awi import SCMLAWI
from scml.scml2019.common import SCMLAgreement, INVALID_UTILITY, CFP, Loan, ProductionFailure
from scml.scml2019.consumers import ScheduleDrivenConsumer, ConsumptionProfile
from scml.scml2019.schedulers import Scheduler, ScheduleInfo, GreedyScheduler
from scml.scml2019.simulators import FactorySimulator, FastFactorySimulator, temporary_transaction
from .builtins import DoNothingFactoryManager, NegotiatorUtility, PessimisticNegotiatorUtility, \
OptimisticNegotiatorUtility, AveragingNegotiatorUtility
class ProductData:
id: int
stock: int
minstock: int
SellThreshold: int
BuyThreshold: int
MinPrice: int
MaxPrice: int
HistoryMin: []
HistoryMax: []
#HistoryPublishers: []
Asked: int
retracted: int
lastSigned: int
source_of: int
transformable: bool
prevMax: int
prevMin: int
def __init__(self, id, SellThreshold, BuyThreshold, MinPrice, MaxPrice, minstock = 0):
self.id = id
self.stock = 0
self.minstock = minstock
self.SellThreshold = SellThreshold
self.BuyThreshold = BuyThreshold
self.MinPrice = MinPrice
self.MaxPrice = MaxPrice
self.HistoryMin = []
self.HistoryMax = []
#self.HistoryPublishers = []
self.Asked = -10
self.retracted = -10
self.source_of = -1
self.transformable = False
self.prevMax = MaxPrice
self.prevMin = MinPrice
self.lastSigned = -1
[docs]class SAHAFactoryManager(DoNothingFactoryManager):
"""The default factory manager that will be implemented by the committee of ANAC-SCML 2019"""
[docs] def on_production_failure(self, failures: List[ProductionFailure]) -> None:
pass
[docs] def confirm_loan(self, loan: Loan, bankrupt_if_rejected: bool) -> bool:
return bankrupt_if_rejected
[docs] def confirm_contract_execution(self, contract: Contract) -> bool:
return True
[docs] def set_renegotiation_agenda(self, contract: Contract, breaches: List[Breach]) -> Optional[RenegotiationRequest]:
return None
[docs] def respond_to_renegotiation_request(self, contract: Contract, breaches: List[Breach],
agenda: RenegotiationRequest) -> Optional[Negotiator]:
return None
def __init__(self, name=None, simulator_type: Union[str, Type[FactorySimulator]] = FastFactorySimulator
, scheduler_type: Union[str, Type[Scheduler]] = GreedyScheduler
, scheduler_params: Optional[Dict[str, Any]] = None
, optimism: float = 0.0
, negotiator_type: Union[str, Type[Negotiator]] = 'negmas.sao.AspirationNegotiator'
, negotiator_params: Optional[Dict[str, Any]] = None
, n_retrials=5, use_consumer=True, reactive=True, sign_only_guaranteed_contracts=False
, riskiness=0.0, max_insurance_premium: float = -1.0):
super().__init__(name=name, simulator_type=simulator_type)
self.negotiator_type = get_class(negotiator_type, scope=globals())
self.negotiator_params = negotiator_params if negotiator_params is not None else {}
self.optimism = optimism
self.ufun_factory: Union[Type[NegotiatorUtility], Callable[[Any, Any], NegotiatorUtility]]
if optimism < 1e-6:
self.ufun_factory = PessimisticNegotiatorUtility
elif optimism > 1 - 1e-6:
self.ufun_factory = OptimisticNegotiatorUtility
else:
self.ufun_factory: NegotiatorUtility = lambda agent, annotation: \
AveragingNegotiatorUtility(agent=agent, annotation=annotation, optimism=self.optimism)
self.max_insurance_premium = max_insurance_premium
self.n_retrials = n_retrials
self.n_neg_trials: Dict[str, int] = defaultdict(int)
self.consumer = None
self.use_consumer = use_consumer
self.reactive = reactive
self.sign_only_guaranteed_contracts = sign_only_guaranteed_contracts
self.contract_schedules: Dict[str, ScheduleInfo] = {}
self.riskiness = riskiness
self.negotiation_margin = int(round(n_retrials * max(0.0, 1.0 - riskiness)))
self.scheduler_type: Type[Scheduler] = get_class(scheduler_type, scope=globals())
self.scheduler: Scheduler = None
self.scheduler_params: Dict[str, Any] = scheduler_params if scheduler_params is not None else {}
self.cfp_records = {}
self.maxdebt = 0
self.firstArrival = -1
self.lastLine = 0
[docs] def total_utility(self, contracts: Collection[Contract] = ()) -> float:
"""Calculates the total utility for the agent of a collection of contracts"""
if self.scheduler is None:
raise ValueError('Cannot calculate total utility without a scheduler')
min_concluded_at = self.awi.current_step
min_sign_at = min_concluded_at + self.awi.default_signing_delay
with temporary_transaction(self.scheduler):
schedule = self.scheduler.schedule(contracts=contracts, assume_no_further_negotiations=False
, ensure_storage_for=self.transportation_delay
, start_at=min_sign_at)
if not schedule.valid:
return INVALID_UTILITY
return schedule.final_balance
[docs] def init(self):
if self.use_consumer:
# @todo add the parameters of the consumption profile as parameters of the greedy factory manager
consumer_products = dict(zip(self.consuming.keys(),
(ConsumptionProfile(schedule=[_] * self.awi.n_steps) for _ in itertools.repeat(0))))
a = dict(zip([self.awi.products[0].id],
(ConsumptionProfile(schedule=[_] * self.awi.n_steps) for _ in itertools.repeat(0))))
consumer_products[self.awi.products[0].id] = a[self.awi.products[0].id]
self.consumer: ScheduleDrivenConsumer = ScheduleDrivenConsumer(profiles=consumer_products
, consumption_horizon=self.awi.n_steps,
immediate_cfp_update=True
, name=self.name)
self.consumer.id = self.id
self.consumer.awi = self.awi
self.consumer.init_()
self.scheduler = self.scheduler_type(manager_id=self.id, awi=self.awi
, max_insurance_premium=self.max_insurance_premium
, **self.scheduler_params)
self.scheduler.init(simulator=self.simulator, products=self.products, processes=self.processes
, producing=self.producing, profiles=self.compiled_profiles)
#print(self.name, " consume: ", self.consuming.keys(), " produce: ", self.producing.keys())
raw_product = min(self.products, key=attrgetter("production_level"))
if raw_product.id not in self.consuming.keys():
self.cfp_records[raw_product.id] = ProductData(raw_product.id, 2, 0.3, raw_product.catalog_price,
raw_product.catalog_price)
refined_product = max(self.products, key=attrgetter("production_level"))
if refined_product.id not in self.producing.keys():
self.cfp_records[refined_product.id] = ProductData(refined_product.id, 2, 0.3,
refined_product.catalog_price,
refined_product.catalog_price)
for product in self.consuming.keys():
self.cfp_records[self.products[product].id] = \
ProductData(product, 2, 0.3, self.products[product].catalog_price,
self.products[product].catalog_price, 12)
self.cfp_records[self.products[product].id].transformable = True
for product in self.producing.keys():
self.cfp_records[self.products[product].id] = \
ProductData(product, 2, 0.3, self.products[product].catalog_price,
self.products[product].catalog_price)
for key, product in self.cfp_records.items():
for item in self.products:
if item.production_level == self.products[product.id].production_level + 1:
product.source_of = item.id
break
[docs] def on_contract_breached(self, contract: Contract, breaches: List[Breach], resolution: Optional[Contract]):
cfp = contract.annotation["cfp"]
"""print("breach! step:", self.awi.current_step, " seller: ", contract.annotation["seller"], " buyer: ",
contract.annotation["buyer"], " quantity: ", contract.agreement['quantity'], "product: ", cfp.product, "stock: ",
self.cfp_records[cfp.product].stock, " real stock: ", self.awi.state.storage[cfp.product])
"""
if contract.annotation["seller"] == self.id:
self.cfp_records[cfp.product].stock += contract.agreement['quantity']
else:
if self.cfp_records[cfp.product].transformable:
self.cfp_records[self.cfp_records[cfp.product].source_of].stock -= contract.agreement['quantity']
else:
self.cfp_records[cfp.product].stock -= contract.agreement['quantity']
[docs] def respond_to_negotiation_request(self, cfp: "CFP", partner: str) -> Optional[Negotiator]:
if self.awi.is_bankrupt(partner):
return None
if self.use_consumer:
return self.consumer.respond_to_negotiation_request(cfp=cfp, partner=partner)
else:
neg = self.negotiator_type(name=self.name + '*' + partner, **self.negotiator_params)
ufun = self.ufun_factory(self, self._create_annotation(cfp=cfp))
neg.utility_function = normalize(ufun,
outcomes=cfp.outcomes, infeasible_cutoff=0)
return neg
[docs] def on_negotiation_success(self, contract: Contract, mechanism: AgentMechanismInterface):
if self.use_consumer:
self.consumer.on_negotiation_success(contract, mechanism)
[docs] def on_negotiation_failure(self, partners: List[str], annotation: Dict[str, Any], mechanism: AgentMechanismInterface
, state: MechanismState) -> None:
cfp = annotation['cfp']
"""print("negotiation fail. Timeout: ", mechanism.state.timedout, " Seller: ", annotation['seller'],
" buyer: ", annotation['buyer'], " quantity: ", cfp.quantity, " product: ", cfp.product,
" prices: ", cfp.unit_price, " time: ", cfp.time, " step: ", self.awi.current_step)
"""
if mechanism.state.timedout:
return
def _execute_schedule(self, schedule: ScheduleInfo, contract: Contract) -> None:
if self.simulator is None:
raise ValueError('No factory simulator is defined')
awi: SCMLAWI = self.awi
total = contract.agreement['unit_price'] * contract.agreement['quantity']
product = contract.annotation['cfp'].product
if contract.annotation['buyer'] == self.id:
self.simulator.buy(product=product, quantity=contract.agreement['quantity']
, price=total, t=contract.agreement['time']
)
if total <= 0 or self.max_insurance_premium < 0.0 or contract is None:
return
premium = awi.evaluate_insurance(contract=contract)
if premium is None:
return
relative_premium = premium / total
if relative_premium <= self.max_insurance_premium:
awi.buy_insurance(contract=contract)
self.simulator.pay(premium, self.awi.current_step)
return
# I am a seller
self.simulator.sell(product=product, quantity=contract.agreement['quantity']
, price=total, t=contract.agreement['time'])
for job in schedule.jobs:
if job.action == 'run':
awi.schedule_job(job, contract=contract)
elif job.action == 'stop':
awi.stop_production(line=job.line, step=job.time, contract=contract, override=job.override)
else:
awi.schedule_job(job, contract=contract)
self.simulator.schedule(job=job, override=False)
for need in schedule.needs:
self.awi.schedule_production(need.product,self.awi.current_step,contract)
[docs] def sign_contract(self, contract: Contract):
if any(self.awi.is_bankrupt(partner) for partner in contract.partners):
return None
product = contract.annotation['cfp'].product
if self.cfp_records[product].stock < -self.maxdebt:
return None
signature = self.id
with temporary_transaction(self.scheduler):
schedule = self.scheduler.schedule(assume_no_further_negotiations=False, contracts=[contract]
, ensure_storage_for=self.transportation_delay
, start_at=self.awi.current_step + 1)
if self.sign_only_guaranteed_contracts and (not schedule.valid or len(schedule.needs) > 1):
self.awi.logdebug(f'{self.name} refused to sign contract {contract.id} because it cannot be scheduled')
return None
if schedule.valid:
profit = schedule.final_balance - self.simulator.final_balance
self.awi.logdebug(f'{self.name} singing contract {contract.id} expecting '
f'{-profit if profit < 0 else profit} {"loss" if profit < 0 else "profit"}')
else:
self.awi.logdebug(f'{self.name} singing contract {contract.id} expecting breach')
return None
self.contract_schedules[contract.id] = schedule
return signature
[docs] def on_contract_signed(self, contract: Contract):
cfp = contract.annotation["cfp"]
"""print("contract signed. Seller: ", contract.annotation['seller'], " buyer: ", contract.annotation['buyer'],
" quantity: ", contract.agreement['quantity'], " product: ", cfp.product, " price: ", contract.agreement['unit_price'],
" time: ", contract.agreement['time'], " step: ", self.awi.current_step)
"""
if contract.annotation['buyer'] == self.id:
if cfp.product in self.consuming:
self.cfp_records[self.cfp_records[cfp.product].source_of].stock += contract.agreement['quantity']
if self.firstArrival == -1:
self.firstArrival = contract.agreement['time']
else:
self.cfp_records[cfp.product].stock += contract.agreement['quantity']
else:
self.cfp_records[cfp.product].stock -= contract.agreement['quantity']
self.cfp_records[cfp.product].prevMin = self.cfp_records[cfp.product].MinPrice
self.cfp_records[cfp.product].prevMax = self.cfp_records[cfp.product].MaxPrice
if cfp.min_unit_price not in self.cfp_records[cfp.product].HistoryMin:
self.cfp_records[cfp.product].HistoryMin.append(cfp.min_unit_price)
if cfp.max_unit_price not in self.cfp_records[cfp.product].HistoryMax:
self.cfp_records[cfp.product].HistoryMax.append(cfp.max_unit_price)
self.recalculate_prices(cfp.product)
self.cfp_records[cfp.product].lastSigned = self.awi.current_step
if contract.annotation['buyer'] == self.id and self.use_consumer:
self.consumer.on_contract_signed(contract)
schedule = self.contract_schedules[contract.id]
if schedule is not None and schedule.valid:
self._execute_schedule(schedule=schedule, contract=contract)
if contract.annotation['buyer'] != self.id or not self.use_consumer:
for negotiation in self._running_negotiations.values():
self.notify(negotiation.negotiator, Notification(type='ufun_modified', data=None))
def _process_buy_cfp(self, cfp: 'CFP') -> None:
if self.awi.is_bankrupt(cfp.publisher):
return None
"""if self.simulator is None or not self.can_expect_agreement(cfp=cfp, margin=self.negotiation_margin):
return
if not self.can_produce(cfp=cfp):
return"""
if self.awi.n_steps - 3 <= self.awi.current_step and self.cfp_records[cfp.product].stock < cfp.min_quantity:
return None
if self.cfp_records[cfp.product].stock < -self.maxdebt:
return None
if cfp.max_time < 4 + (2 * cfp.product) or self.firstArrival >= cfp.max_time:
return None
cfp.unit_price = self.generate_price_ranges(self.cfp_records[cfp.product].MaxPrice - self.cfp_records[cfp.product].MaxPrice * self.cfp_records[cfp.product].BuyThreshold,
self.cfp_records[cfp.product].MaxPrice + self.cfp_records[cfp.product].MaxPrice * self.cfp_records[cfp.product].SellThreshold)
if self.negotiator_type == AspirationNegotiator:
neg = self.negotiator_type(assume_normalized=True, name=self.name + '>' + cfp.publisher)
else:
neg = self.negotiator_type(name=self.name + '>' + cfp.publisher)
self.request_negotiation(negotiator=neg, cfp=cfp
, ufun=normalize(self.ufun_factory(self, self._create_annotation(cfp=cfp))
, outcomes=cfp.outcomes, infeasible_cutoff=-1500))
def _process_sell_cfp(self, cfp: 'CFP'):
if self.awi.is_bankrupt(cfp.publisher):
return None
if self.awi.n_steps - 4 <= self.awi.current_step:
return None
cfp.unit_price = self.generate_price_ranges(0,
self.cfp_records[cfp.product].MinPrice + self.cfp_records[cfp.product].MinPrice * self.cfp_records[cfp.product].BuyThreshold)
if self.negotiator_type == AspirationNegotiator:
neg = self.negotiator_type(assume_normalized=True, name=self.name + '>' + cfp.publisher)
else:
neg = self.negotiator_type(name=self.name + '>' + cfp.publisher)
self.request_negotiation(negotiator=neg, cfp=cfp
, ufun=normalize(self.ufun_factory(self, self._create_annotation(cfp=cfp))
, outcomes=cfp.outcomes, infeasible_cutoff=-1500))
[docs] def on_new_cfp(self, cfp: 'CFP') -> None:
if cfp.product in self.cfp_records:
#self.cfp_records[cfp.product].HistoryPublishers.append(cfp.publisher)
if cfp.min_unit_price not in self.cfp_records[cfp.product].HistoryMin:
self.cfp_records[cfp.product].HistoryMin.append(cfp.min_unit_price)
if cfp.max_unit_price not in self.cfp_records[cfp.product].HistoryMax:
self.cfp_records[cfp.product].HistoryMax.append(cfp.max_unit_price)
self.recalculate_prices(cfp.product)
want = [value.id for key, value in self.cfp_records.items() if value.stock > 0]
want += list(self.producing.keys())
if cfp.satisfies(query={'is_buy': True, 'products': want}):
self._process_buy_cfp(cfp)
want = [value.id for key, value in self.cfp_records.items()]
if cfp.satisfies(query={'is_buy': False, 'products': want}):
self._process_sell_cfp(cfp)
[docs] def recalculate_prices(self, product):
if len(self.cfp_records[product].HistoryMin) > 0:
self.cfp_records[product].MinPrice = max([math.floor(min(self.cfp_records[product].HistoryMin)), 1])
if len(self.cfp_records[product].HistoryMax) > 0:
self.cfp_records[product].MaxPrice = min([math.ceil(max(self.cfp_records[product].HistoryMax)), 1000])
[docs] def generate_price_ranges(self, minPrice, maxPrice):
stepSize = (maxPrice-minPrice)/19
prices = []
numSteps = int(min([19, 2 * (maxPrice-minPrice)]))
if numSteps < 10:
stepSize = 0.5
for step in range(numSteps+1):
prices.append(round(minPrice + step*stepSize, 2))
if numSteps < 10:
prices.append(maxPrice)
return prices
[docs] def on_contract_executed(self, contract: Contract):
"""print("contract executed!!!. Seller: ", contract.annotation['seller'], " buyer: ", contract.annotation['buyer'],
" quantity: ", contract.agreement['quantity'], " product: ", contract.annotation['cfp'].product,
" price: ", contract.agreement['unit_price'], " time: ", contract.agreement['time'],
" step: ", self.awi.current_step)
"""
if self.cfp_records[contract.annotation['cfp'].product].transformable:
for i in range(min([contract.agreement['quantity'], 10])):
self.awi.schedule_production(self.lastLine, self.awi.current_step+1)
self.lastLine = (self.lastLine + 1) % 10
[docs] def step(self):
self.maxdebt = max([0, 30 - 30 * self.awi.current_step/self.awi.n_steps])
for key, item in self.cfp_records.items():
if item.lastSigned < self.awi.current_step - 4 and item.retracted < self.awi.current_step - 4:
if len(item.HistoryMin) > 1:
item.HistoryMin.remove(min(item.HistoryMin))
#item.HistoryMin.pop()
if len(item.HistoryMax) > 1:
#item.HistoryMax.pop()
item.HistoryMax.remove(max(item.HistoryMax))
self.recalculate_prices(key)
item.retracted = self.awi.current_step
#print("step: ", self.awi.current_step)
for key, item in self.cfp_records.items():
if item.transformable:
stock = self.cfp_records[item.source_of].stock
if key in self.awi.state.storage:
for i in range(min([10,self.awi.state.storage[key]])):
self.awi.schedule_production(self.lastLine, self.awi.current_step+1)
self.lastLine = (self.lastLine + 1) % 10
else:
stock = item.stock
if item.id not in self.producing.keys() and stock <= item.minstock \
and (item.Asked < self.awi.current_step - 2) and self.awi.n_steps - 6 > self.awi.current_step:
queries = 1
if item.id in self.consuming.keys():
queries = 3
for i in range(queries):
cfp = CFP(is_buy=True, publisher=self.id, product=item.id
, time= (self.awi.current_step + 4 +i, min([self.awi.current_step + 6+i, self.awi.n_steps-1]))
, unit_price=self.generate_price_ranges(0,
self.products[item.id].catalog_price + self.products[item.id].catalog_price
* self.cfp_records[item.id].BuyThreshold)
, quantity=3)
self.awi.register_cfp(cfp)
item.Asked = self.awi.current_step
if item.stock > 0 and item.id not in self.consuming.keys():
for unit in range(item.stock):
unit_price = self.generate_price_ranges(item.MaxPrice - item.MaxPrice * item.BuyThreshold,
item.MaxPrice + item.MaxPrice * item.SellThreshold)
cfp = CFP(is_buy=False, publisher=self.id, product=item.id
, time=max([2 + (2 * item.id), self.awi.current_step + 6]),
unit_price=unit_price, quantity=1)
self.awi.register_cfp(cfp)
item.Asked = self.awi.current_step
[docs] def can_produce(self, cfp: CFP, assume_no_further_negotiations=False) -> bool:
"""Whether or not we can produce the required item in time"""
if cfp.product not in self.producing.keys():
return False
agreement = SCMLAgreement(time=cfp.max_time, unit_price=cfp.max_unit_price, quantity=cfp.min_quantity)
min_concluded_at = self.awi.current_step + 1 - int(self.immediate_negotiations)
min_sign_at = min_concluded_at + self.awi.default_signing_delay
if cfp.max_time < min_sign_at + 1: # 1 is minimum time to produce the product
return False
with temporary_transaction(self.scheduler):
schedule = self.scheduler.schedule(contracts=[Contract(partners=[self.id, cfp.publisher]
, agreement=agreement
, annotation=self._create_annotation(cfp=cfp)
, issues=cfp.issues, signed_at=min_sign_at
, concluded_at=min_concluded_at)]
, ensure_storage_for=self.transportation_delay
, assume_no_further_negotiations=assume_no_further_negotiations
, start_at=min_sign_at)
return schedule.valid and self.can_secure_needs(schedule=schedule, step=self.awi.current_step)
[docs] def can_secure_needs(self, schedule: ScheduleInfo, step: int):
"""
Finds if it is possible in principle to arrange these needs at the given time.
Args:
schedule:
step:
Returns:
"""
needs = schedule.needs
if len(needs) < 1:
return True
for need in needs:
if need.quantity_to_buy > 0 and need.step < step + 1 - int(self.immediate_negotiations): # @todo check this
return False
return True