Source code for scml.scml2020.agents.decentralizing

"""
Implements the `DecentralizingAgent` which creates ony buy and one sell controller for each time-step and relinquishes
control of negotiations to buy/sell the required number of items of its input/output product.
"""

import copy
import itertools
import math
import random
from collections import defaultdict, namedtuple
from dataclasses import dataclass
from typing import Union, Optional, Dict, Any, List, Iterable, Tuple, Type

import numpy as np
from negmas import (
    UtilityFunction,
    Outcome,
    outcome_as_dict,
    LinearUtilityFunction,
    SAONegotiator,
    Issue,
    Negotiator,
    AgentMechanismInterface,
    Contract,
    SAOController,
    MechanismState,
    ResponseType,
    AspirationMixin,
    AspirationNegotiator,
    PassThroughNegotiator,
)
from negmas.events import Notifier, Notification
from negmas.helpers import instantiate, get_class
from pprint import pformat
from scml.scml2020 import AWI, NO_COMMAND, ANY_LINE

from .do_nothing import DoNothingAgent

QUANTITY = 0
TIME = 1
UNIT_PRICE = 2

__all__ = ["DecentralizingAgent"]


class StepController(SAOController, AspirationMixin, Notifier):
    """A controller for managing a set of negotiations about selling/buying the a product starting/ending at some
    specific time-step. It works in conjunction with the `DecentralizingAgent` .

    Args:

        target_quantity: The quantity to be secured
        is_seller:  Is this a seller or a buyer
        parent: The parent `DecedntralizingAgent`
        step:  The simulation step that this controller is responsible about
        urange: The range of unit prices used for negotiation
        product: The product that this controller negotiates about
        partners: A list of partners to negotiate with
        negotiator_type: The type of the negotiator used for all negotiations.
        negotiator_params: The parameters of the negotiator used for all negotiations
        max_retries: How many times can the controller try negotiating with each partner.
        *args: Position arguments passed to the base Controller constructor
        **kwargs: Keyword arguments passed to the base Controller constructor


    Remarks:

        - It uses whatever negotiator type on all of its negotiations and it assumes that the ufun will never change
        - Once it accumulates the required quantity, it ends all remaining negotiations
        - It assumes that all ufuns are identical so there is no need to keep a separate negotiator for each one and it
          instantiates a single negotiator that dynamically changes the AMI but always uses the same ufun.

    """

    def __init__(
        self,
        *args,
        target_quantity: int,
        is_seller: bool,
        parent: "DecentralizingAgent",
        step: int,
        urange: Tuple[int, int],
        product: int,
        partners: List[str],
        negotiator_type: SAONegotiator,
        negotiator_params: Dict[str, Any] = None,
        max_retries: int = 2,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self.__parent = parent
        self.is_seller = is_seller
        self.target = target_quantity
        self.urange = urange
        self.partners = partners
        self.product = product
        negotiator_params = (
            negotiator_params if negotiator_params is not None else dict()
        )
        self.secured = 0
        if is_seller:
            self.ufun = LinearUtilityFunction((1, 1, 10))
        else:
            self.ufun = LinearUtilityFunction((1, -1, -10))
        negotiator_params["ufun"] = self.ufun
        self.__negotiator = instantiate(negotiator_type, **negotiator_params)
        self.completed = defaultdict(bool)
        self.step = step
        self.retries: Dict[str, int] = defaultdict(int)
        self.max_retries = max_retries

    def join(
        self,
        negotiator_id: str,
        ami: AgentMechanismInterface,
        state: MechanismState,
        *,
        ufun: Optional["UtilityFunction"] = None,
        role: str = "agent",
    ) -> bool:
        joined = super().join(negotiator_id, ami, state, ufun=ufun, role=role)
        if joined:
            self.completed[negotiator_id] = False
        return joined

    def propose(self, negotiator_id: str, state: MechanismState) -> Optional["Outcome"]:
        self.__negotiator._ami = self.negotiators[negotiator_id][0]._ami
        return self.__negotiator.propose(state)

    def respond(
        self, negotiator_id: str, state: MechanismState, offer: "Outcome"
    ) -> ResponseType:
        if self.secured >= self.target:
            return ResponseType.END_NEGOTIATION
        self.__negotiator._ami = self.negotiators[negotiator_id][0]._ami
        return self.__negotiator.respond(offer=offer, state=state)

    def __str__(self):
        return (
            f"{'selling' if self.is_seller else 'buying'} p{self.product} [{self.step}] "
            f"secured {self.secured} of {self.target} for {self.__parent.name} "
            f"({len([_ for _ in self.completed.values() if _])} completed of {len(self.completed)} negotiators)"
        )

    def create_negotiator(
        self,
        negotiator_type: Union[str, Type[PassThroughNegotiator]] = None,
        name: str = None,
        cntxt: Any = None,
        **kwargs,
    ) -> PassThroughNegotiator:
        neg = super().create_negotiator(negotiator_type, name, cntxt, **kwargs)
        self.completed[neg.id] = False
        return neg

    def negotiation_concluded(
        self, negotiator_id: str, agreement: Dict[str, Any]
    ) -> None:
        awi: AWI
        awi = self.__parent.awi  # type: ignore
        # mark this negotiation as completed
        self.completed[negotiator_id] = True
        # if there is an agreement increase the secured amount and check if we are done.
        if agreement is not None:
            self.secured += agreement["quantity"]
            if self.secured >= self.target:
                awi.loginfo(f"Ending all negotiations on controller {str(self)}")
                # If we are done, end all other negotiations
                for k in self.negotiators.keys():
                    if self.completed[k]:
                        continue
                    self.notify(
                        self.negotiators[k][0], Notification("end_negotiation", None)
                    )
        self.kill_negotiator(negotiator_id, force=True)
        if all(self.completed.values()):
            # If we secured everything, just return control to the agent
            if self.secured >= self.target:
                awi.loginfo(f"Secured Everything: {str(self)}")
                self.__parent.all_negotiations_concluded(self.step, self.is_seller)
                return
            # If we did not secure everything we need yet and time allows it, create new negotiations
            tmin, tmax = self.__parent._trange(self.step, self.is_seller)

            if awi.current_step < tmax + 1 and tmin <= tmax:
                # get a good partner: one that was not retired too much
                random.shuffle(self.partners)
                for other in self.partners:
                    if self.retries[other] <= self.max_retries:
                        partner = other
                        break
                else:
                    return
                self.retries[partner] += 1
                neg = self.create_negotiator()
                self.completed[neg.id] = False
                awi.loginfo(
                    f"{str(self)} negotiating with {partner} on u={self.urange}"
                    f", q=(1,{self.target-self.secured}), u=({tmin}, {tmax})"
                )
                awi.request_negotiation(
                    not self.is_seller,
                    product=self.product,
                    quantity=(1, self.target - self.secured),
                    unit_price=self.urange,
                    time=(tmin, tmax),
                    partner=partner,
                    negotiator=neg,
                    extra=dict(controller_index=self.step, is_seller=self.is_seller),
                )


@dataclass
class ControllerInfo:
    """Keeps a record of information about one of the controllers used by the agent"""

    controller: StepController
    time_step: int
    is_seller: bool
    time_range: Tuple[int, int]
    target: int
    expected: int
    done: bool = False


[docs]class DecentralizingAgent(DoNothingAgent): """An agent that keeps schedules of what it needs to buy and sell and tries to satisfy them. It assumes that the agent can run a single process. Args: negotiator_type: The negotiator type to use for all negotiations negotiator_params: The parameters used to initialize all negotiators horizon: The number of steps in the future to consider for securing inputs and outputs. predicted_demand: A prediction of the number of units needed by the market of the output product at each timestep predicted_supply: A prediction of the nubmer of units available within the market of the input product at each timestep agreement_fraction: A prediction about the fraction of the quantity negotiated about that will be secured adapt_prices: If true, the agent tries to adapt the unit price range it negotites about to market conditions (i.e. previous trade). If false, catalog prices will be used to constrain the unit price ranges to (1, catalog price) for buying and (catalog price, 2* catalog price) for selling *args: Position arguments to pass the the base `SCML2020Agent` constructor **kwargs: Keyword arguments to pass to the base `SCML2020Agent` constructor """ def __init__( self, *args, negotiator_type: Union[SAONegotiator, str] = AspirationNegotiator, negotiator_params: Optional[Dict[str, Any]] = None, horizon=5, predicted_demand: Union[int, np.ndarray] = None, predicted_supply: Union[int, np.ndarray] = None, agreement_fraction: float = 0.5, adapt_prices: bool = False, **kwargs, ): super().__init__(*args, **kwargs) self.adapt_prices = adapt_prices self.predicted_demand = predicted_demand self.predicted_supply = predicted_supply self.negotiator_type = get_class(negotiator_type) self.negotiator_params = ( negotiator_params if negotiator_params is not None else dict() ) self.horizon = horizon self.exogenous_horizon = None self.input_product: int = -1 self.output_product: int = -1 self.process: int = -1 self.pcost: int = -1 self.n_inputs: int = -1 self.n_outputs: int = -1 self.supplies_needed: np.ndarray = None self.sales_needed: np.ndarray = None self.input_cost: np.ndarray = None self.output_price: np.ndarray = None self.supplies_secured: np.ndarray = None self.sales_secured: np.ndarray = None self.production_needed = None self.production_secured = None self.production_factor = 1 self.buyers = self.sellers = None self.catalog_n_equivalent = 0 self.supplies_negotiating = None self.sales_negotiating = None self.agreement_fraction = agreement_fraction self.use_exogenous_contracts = True
[docs] def init(self): awi: AWI awi = self.awi # type: ignore self.exogenous_horizon = awi.bb_read("settings", "exogenous_horizon") self.buyers: List[ControllerInfo] = [ ControllerInfo(None, i, False, tuple(), 0, 0, False) for i in range(self.awi.n_steps) ] self.sellers: List[ControllerInfo] = [ ControllerInfo(None, i, True, tuple(), 0, 0, False) for i in range(self.awi.n_steps) ] self.catalog_n_equivalent = self.awi.n_steps * 2 self.input_product = int(awi.my_input_product) self.output_product = self.input_product + 1 def adjust(x, demand): if x is None: x = max(1, awi.n_lines // 2) elif isinstance(x, Iterable): return np.array(x) predicted = int(x) * np.ones(awi.n_steps, dtype=int) if demand: predicted[: self.input_product + 1] = 0 else: predicted[self.input_product - awi.n_processes :] = 0 return predicted self.predicted_demand = adjust(self.predicted_demand, True) self.predicted_supply = adjust(self.predicted_supply, False) self.use_exogenous_contracts = awi.bb_read( "settings", "has_exogenous_contracts" ) if ( not self.use_exogenous_contracts and (self.input_product == 0 or self.output_product == awi.n_products - 1) ): self.predicted_supply = np.zeros(awi.n_steps, dtype=int) self.predicted_demand = np.zeros(awi.n_steps, dtype=int) self.process = self.input_product self.pcost = int(np.ceil(np.mean(awi.profile.costs[:, self.process]))) self.n_inputs = awi.inputs[self.process] self.n_outputs = awi.outputs[self.process] self.production_factor = self.n_outputs / self.n_inputs self.supplies_secured = np.zeros(awi.n_steps, dtype=int) self.sales_secured = np.zeros(awi.n_steps, dtype=int) self.supplies_secured[ : self.exogenous_horizon ] = awi.profile.exogenous_supplies[: self.exogenous_horizon, self.input_product] self.sales_secured[: self.exogenous_horizon] = awi.profile.exogenous_sales[ : self.exogenous_horizon, self.output_product ] self.supplies_needed = np.zeros(awi.n_steps, dtype=int) self.sales_needed = np.zeros(awi.n_steps, dtype=int) self.production_needed = np.zeros(awi.n_steps, dtype=int) self.production_secured = np.zeros(awi.n_steps, dtype=int) if awi.my_input_product != 0 or self.use_exogenous_contracts: self.supplies_needed[:-1] = np.floor( self.predicted_demand[1:] / self.production_factor ).astype(int) if awi.my_output_product != awi.n_products - 1 or self.use_exogenous_contracts: self.sales_needed[1:] = np.floor( self.predicted_supply[:-1] * self.production_factor ).astype(int) self.supplies_needed[:-1] += np.ceil( self.sales_secured[1:] / self.production_factor ).astype(int) self.sales_needed[1:] += np.floor( self.supplies_secured[:-1] * self.production_factor ).astype(int) self.supplies_negotiating = np.zeros_like(self.supplies_needed) self.sales_negotiating = np.zeros_like(self.sales_needed) self.production_needed[:-1] = np.minimum( self.supplies_needed[:-1], self.sales_needed[1:] ) inprices = awi.profile.exogenous_supply_prices[:, self.input_product] inprices[self.supplies_secured == 0] = 0 outprices = awi.profile.exogenous_sale_prices[:, self.output_product] outprices[self.sales_secured == 0] = 0 self.input_cost = np.maximum( inprices, self.awi.catalog_prices[self.input_product] ) self.output_price = np.maximum( outprices, self.awi.catalog_prices[self.output_product] ) self.awi.logdebug_agent(f"Initialized\n{pformat(self._debug_state())}")
def _debug_state(self): return { "supplies_secured": self.supplies_secured.tolist(), "supplies_needed": self.supplies_needed.tolist(), "supplies_negotiating": self.supplies_negotiating.tolist(), "sales_secured": self.sales_secured.tolist(), "sales_needed": self.sales_needed.tolist(), "sales_negotiating": self.sales_negotiating.tolist(), "production_secured": self.production_secured.tolist(), "production_needed": self.production_needed.tolist(), "buyers": [ f"step: {_.controller.step} secured {_.controller.secured} of {_.controller.target} units " f"[Completed {len([_ for _ in _.controller.completed.values() if _])} " f"of {len(_.controller.completed)}]" for _ in self.buyers if _ is not None and _.controller is not None ], "sellers": [ f"step: {_.controller.step} secured {_.controller.secured} of {_.controller.target} units " f"[Completed {len([_ for _ in _.controller.completed.values() if _])} " f"of {len(_.controller.completed)}]" for _ in self.sellers if _ is not None and _.controller is not None ], "buy_negotiations": [ _.annotation["seller"] for _ in self.running_negotiations if _.annotation["buyer"] == self.id ], "sell_negotiations": [ _.annotation["buyer"] for _ in self.running_negotiations if _.annotation["seller"] == self.id ], "_balance": self.awi.state.balance, "_input_inventory": self.awi.state.inventory[self.awi.my_input_product], "_output_inventory": self.awi.state.inventory[self.awi.my_output_product], }
[docs] def step(self): """Generates buy and sell negotiations as needed""" self.awi.logdebug_agent(f"Enter step:\n{pformat(self._debug_state())}") s = self.awi.current_step if self.exogenous_horizon != self.awi.n_steps and self.use_exogenous_contracts: nxt = s + self.exogenous_horizon if nxt < self.awi.n_steps: self.supplies_secured[nxt] += self.awi.profile.exogenous_supplies[nxt] if nxt + 1 < self.awi.n_steps: self.sales_needed[nxt + 1] += self.awi.profile.exogenous_supplies[ nxt ] self.sales_secured[nxt] += self.awi.profile.exogenous_sales[nxt] if nxt - 1 >= 0: self.supplies_needed[nxt - 1] += self.awi.profile.exogenous_sales[ nxt ] if s == 0: last = min(self.awi.n_steps - 1, self.horizon + 2) for step in range(1, last): self.generate_buy_negotiations(step) self.generate_sell_negotiations(step) else: nxt = s + self.horizon + 1 if nxt > self.awi.n_steps - 1: self.awi.logdebug_agent(f"End step:\n{pformat(self._debug_state())}") return self.generate_buy_negotiations(nxt) self.generate_sell_negotiations(nxt) self.awi.logdebug_agent(f"End step:\n{pformat(self._debug_state())}")
[docs] def respond_to_negotiation_request( self, initiator: str, issues: List[Issue], annotation: Dict[str, Any], mechanism: AgentMechanismInterface, ) -> Optional[Negotiator]: # find negotiation parameters is_seller = annotation["seller"] == self.id tmin, tmax = issues[TIME].min_value, issues[TIME].max_value + 1 # find the time-step for which this negotiation should be added step = max(0, tmin - 1) if is_seller else min(self.awi.n_steps - 1, tmax + 1) # find the corresponding controller. controller_info: ControllerInfo controller_info = self.sellers[step] if is_seller else self.buyers[step] # check if we need to negotiate and indicate that we are negotiating some amount if we need if is_seller: assert annotation["product"] == self.output_product target = ( self.sales_needed[tmin : tmax + 1].sum() - self.sales_secured[tmin : tmax + 1].sum() ) if target <= 0: return None self.sales_negotiating[tmin : tmax + 1] += int( math.ceil(self.agreement_fraction * issues[QUANTITY].max_value) // (tmax + 1 - tmin) ) else: assert annotation["product"] == self.input_product target = ( self.supplies_needed[tmin : tmax + 1].sum() - self.supplies_secured[tmin : tmax + 1].sum() ) if target <= 0: return None self.supplies_negotiating[ issues[TIME].min_value : issues[TIME].max_value + 1 ] += int( math.ceil(self.agreement_fraction * issues[QUANTITY].max_value) ) // ( issues[TIME].max_value + 1 - issues[TIME].min_value ) self.awi.loginfo( f"Accepting request from {initiator}: {[str(_) for _ in mechanism.issues]} " f"({Issue.num_outcomes(mechanism.issues)})" ) # create a controller for the time-step if one does not exist or use the one already running if controller_info.controller is None: controller = self.add_controller( is_seller, target, self._urange(step, is_seller, (tmin, tmax)), int(self.agreement_fraction * target), step, ) else: controller = controller_info.controller # create a new negotiator, add it to the controller and return it return controller.create_negotiator()
[docs] def on_negotiation_failure( self, partners: List[str], annotation: Dict[str, Any], mechanism: AgentMechanismInterface, state: MechanismState, ) -> None: controller = self._get_controller(mechanism) neg = self._running_negotiations[mechanism.id] negotiator_id = neg.negotiator.id controller.negotiation_concluded(negotiator_id, None)
[docs] def on_negotiation_success( self, contract: Contract, mechanism: AgentMechanismInterface ) -> None: controller = self._get_controller(mechanism) neg = self._running_negotiations[mechanism.id] negotiator_id = neg.negotiator.id controller.negotiation_concluded(negotiator_id, contract.agreement)
[docs] def on_contracts_finalized( self, signed: List[Contract], cancelled: List[Contract], rejectors: List[List[str]], ) -> None: self.awi.logdebug_agent( f"Enter Contracts Finalized:\n" f"Signed {pformat([self._format(_) for _ in signed])}\n" f"Cancelled {pformat([self._format(_) for _ in cancelled])}\n" f"{pformat(self._debug_state())}" ) consumed = 0 for contract in signed: is_seller = contract.annotation["seller"] == self.id q, u, t = ( contract.agreement["quantity"], contract.agreement["unit_price"], contract.agreement["time"], ) if is_seller: # if I am a seller, I will schedule production then buy my needs to produce output_product = contract.annotation["product"] input_product = output_product - 1 self.output_price[t] = ( self.output_price[t] * (self.catalog_n_equivalent + self.sales_secured[t]) + u * q ) / (self.sales_secured[t] + q) self.sales_secured[t] += q if input_product >= 0 and t > 0: steps, lines = self.awi.available_for_production( repeats=q, step=(self.awi.current_step, t - 1) ) q = min(len(steps) - consumed, q) consumed += q self.production_needed[t - 1] += q if contract.annotation["caller"] != self.id: self.supplies_needed[t - 1] += max( 1, int(math.ceil(q / self.production_factor)) ) continue # I am a buyer. I need not produce anything but I need to negotiate to sell the production of what I bought input_product = contract.annotation["product"] output_product = input_product + 1 self.input_cost[t] = ( self.input_cost[t] * (self.catalog_n_equivalent + self.supplies_secured[t]) + u * q ) / (self.supplies_secured[t] + q) self.supplies_secured[t] += q if output_product < self.awi.n_products and t < self.awi.n_steps - 1: if contract.annotation["caller"] != self.id: self.sales_needed[t + 1] += max(1, int(q * self.production_factor)) self.awi.logdebug_agent( f"Exit Contracts Finalized:\n{pformat(self._debug_state())}" )
def _format(self, c: Contract): return ( f"{f'>' if c.annotation['seller'] == self.id else '<'}" f"{c.annotation['buyer'] if c.annotation['seller'] == self.id else c.annotation['seller']}: " f"{c.agreement['quantity']} of {c.annotation['product']} @ {c.agreement['unit_price']} on {c.agreement['time']}" )
[docs] def sign_all_contracts(self, contracts: List[Contract]) -> List[Optional[str]]: self.awi.logdebug_agent( f"Enter Sign Contracts {pformat([self._format(_) for _ in contracts])}:\n{pformat(self._debug_state())}" ) contracts = sorted(contracts, key=lambda x: x.agreement["time"]) signatures = [None] * len(contracts) taken = 0 s = self.awi.current_step for i, contract in enumerate(contracts): q, u, t = ( contract.agreement["quantity"], contract.agreement["unit_price"], contract.agreement["time"], ) # check that the contract is executable in principle if t <= s and len(contract.issues) == 3: continue # check that I can produce the required quantities even in principle if contract.annotation["seller"] == self.id: trange = (s, t) secured, needed, negotiating = ( self.sales_secured, self.sales_needed, self.sales_negotiating, ) else: trange = (t + 1, self.awi.n_steps - 1) secured, needed, negotiating = ( self.supplies_secured, self.supplies_needed, self.supplies_negotiating, ) steps, lines = self.awi.available_for_production( q, trange, ANY_LINE, override=False, method="all" ) if len(steps) - taken < q: continue taken += q if ( secured[trange[0] : trange[1] + 1].sum() + q + taken * self.agreement_fraction <= needed[trange[0] : trange[1] + 1].sum() ): signatures[i] = self.id self.awi.logdebug_agent(f"Exit Sign Contracts:\n{pformat(self._debug_state())}") return signatures
[docs] def confirm_production( self, commands: np.ndarray, balance: int, inventory ) -> np.ndarray: commands = np.ones_like(commands) * NO_COMMAND awi: AWI awi = self.awi s = awi.current_step inputs = awi.state.inventory[self.input_product] n_needed = max(0, min(awi.n_lines, self.production_needed[: s + 1].sum())) if inputs < n_needed: if s < awi.n_steps - 1: self.production_needed[s + 1] += inputs - n_needed n_needed = inputs commands[:n_needed] = self.input_product self.production_needed[s] -= n_needed return commands
[docs] def on_contract_nullified( self, contract: Contract, compensation_money: int, new_quantity: int ) -> None: q = contract.agreement["quantity"] if new_quantity < q: t = contract.agreement["time"] missing = q - new_quantity if t < self.awi.current_step: return if contract.annotation["seller"] == self.id: self.sales_secured[t] -= missing if t > 0: self.production_needed[t - 1] -= missing self.supplies_needed[t - 1] -= missing if ( self.sellers[t] is not None and self.sellers[t].controller is not None ): self.sellers[t].controller.target -= missing else: self.supplies_secured[t] += missing if t < self.awi.n_steps - 1: self.sales_needed[t + 1] -= missing if self.buyers[t] is not None and self.buyers[t].controller is not None: self.buyers[t].controller.target += missing
[docs] def all_negotiations_concluded( self, controller_index: int, is_seller: bool ) -> None: """Called by the `StepController` to affirm that it is done negotiating for some time-step""" info = ( self.sellers[controller_index] if is_seller else self.buyers[controller_index] ) info.done = True c = info.controller quantity = c.secured target = c.target expected = info.expected time_range = info.time_range if is_seller: negotiating, secured, needed = ( self.sales_negotiating, self.sales_secured, self.sales_needed, ) controllers, generator = self.sellers, self.generate_sell_negotiations else: negotiating, secured, needed = ( self.supplies_negotiating, self.supplies_secured, self.supplies_needed, ) controllers, generator = self.buyers, self.generate_buy_negotiations if time_range[1] + 1 - time_range[0] > 0: negotiating[time_range[0] : time_range[1] + 1] -= expected // ( time_range[1] + 1 - time_range[0] ) self.awi.loginfo( f"Killing Controller {str(controllers[controller_index].controller)}" ) controllers[controller_index].controller = None if quantity <= target: secured[time_range[0]] += quantity generator(step=controller_index) return
[docs] def confirm_exogenous_sales( self, quantities: np.ndarray, unit_prices: np.ndarray ) -> np.ndarray: p, s = self.output_product, self.awi.current_step quantities[p] = max( 0, min(quantities[p], self.sales_needed[s] - self.sales_secured[s]) ) return quantities
[docs] def confirm_exogenous_supplies( self, quantities: np.ndarray, unit_prices: np.ndarray ) -> np.ndarray: p, s = self.input_product, self.awi.current_step quantities[p] = max( 0, min(quantities[p], self.supplies_needed[s] - self.supplies_secured[s]) ) return quantities
[docs] def generate_buy_negotiations(self, step): """Creates the controller and starts negotiations to acquire all required inputs (supplies) at the given step""" quantity = ( self.supplies_needed[step] - self.supplies_secured[step] - self.supplies_negotiating[step] ) if quantity <= 0: return self.start_negotiations( product=self.input_product, step=step, quantity=max( 1, min( self.awi.n_lines * (step - self.awi.current_step), int(quantity * self.production_factor), ), ), unit_price=int( math.ceil( (self.n_outputs * self.output_price[step] - self.pcost) / self.n_inputs ) ), )
[docs] def generate_sell_negotiations(self, step): """Creates the controller and starts negotiations to sell all required outputs (sales) at the given step""" # find out if I need to sell any output products quantity = ( self.sales_needed[step] - self.sales_secured[step] - self.sales_negotiating[step] ) if quantity <= 0: return # if so, negotiate to sell as much of them as possible self.start_negotiations( product=self.output_product, step=step, quantity=max( 1, min( self.awi.n_lines * (step - self.awi.current_step), int(math.floor(quantity / self.production_factor)), ), ), unit_price=(self.pcost + self.input_cost[step] * self.n_inputs) // self.n_outputs, )
[docs] def start_negotiations( self, product: int, quantity: int, unit_price: int, step: int ) -> None: """ Starts a set of negotiations to by/sell the product with the given limits Args: product: product type. If it is an input product, negotiations to buy it will be started otherweise to sell. quantity: The maximum quantity to negotiate about unit_price: The maximum/minimum unit price for buy/sell step: The maximum/minimum time for buy/sell Remarks: - This method assumes that products cannot be in my_input_products and my_output_products """ awi: AWI awi = self.awi # type: ignore is_seller = product == self.output_product if quantity < 1 or unit_price < 1 or step < awi.current_step + 1: awi.logdebug( f"Less than 2 valid issues (q:{quantity}, u:{unit_price}, t:{step})" ) return # choose ranges for the negotiation agenda. qvalues = (1, quantity) tvalues = self._trange(step, is_seller) uvalues = self._urange(step, is_seller, tvalues) if tvalues[0] > tvalues[1]: return if is_seller: partners = awi.my_consumers expected_quantity = int(math.floor(qvalues[1] * self.agreement_fraction)) else: partners = awi.my_suppliers expected_quantity = int(math.floor(qvalues[1] * self.agreement_fraction)) # negotiate with everyone controller = self.add_controller( is_seller, qvalues[1], uvalues, expected_quantity, step ) awi.loginfo( f"Requesting {'selling' if is_seller else 'buying'} negotiation " f"on u={uvalues}, q={qvalues}, t={tvalues}" f" with {str(partners)} using {str(controller)}" ) self.awi.request_negotiations( is_buy=not is_seller, product=product, quantity=qvalues, unit_price=uvalues, time=tvalues, partners=partners, controller=controller, extra=dict(controller_index=step, is_seller=is_seller), ) if is_seller: self.sales_negotiating[ tvalues[0] : tvalues[1] + 1 ] += expected_quantity // (tvalues[1] + 1 - tvalues[0]) else: self.supplies_negotiating[ tvalues[0] : tvalues[1] + 1 ] += expected_quantity // (tvalues[1] + 1 - tvalues[0])
[docs] def max_production_till(self, step) -> int: """Returns the maximum number of units that can be produced until the given step given current production schedule""" n = self.awi.n_lines * (step - self.awi.current_step + 1) steps, lines = self.awi.available_for_production( repeats=n, step=(self.awi.current_step, step - 1) ) return int(len(steps) * self.production_factor)
[docs] def max_consumption_till(self, step) -> int: """Returns the maximum number of units that can be consumed until the given step given current production schedule""" n = self.awi.n_lines * (step - self.awi.current_step + 1) steps, lines = self.awi.available_for_production( repeats=n, step=(self.awi.current_step, step - 1) ) return int(math.ceil(len(steps) / self.production_factor))
[docs] def add_controller( self, is_seller: bool, target, urange: Tuple[int, int], expected_quantity: int, step: int, ) -> StepController: if is_seller and self.sellers[step].controller is not None: return self.sellers[step].controller if not is_seller and self.buyers[step].controller is not None: return self.buyers[step].controller controller = StepController( is_seller=is_seller, target_quantity=target, negotiator_type=self.negotiator_type, negotiator_params=self.negotiator_params, parent=self, step=step, urange=urange, product=self.output_product if is_seller else self.input_product, partners=self.awi.my_consumers if is_seller else self.awi.my_suppliers, ) if is_seller: assert self.sellers[step].controller is None self.sellers[step] = ControllerInfo( controller, step, is_seller, self._trange(step, is_seller), target, expected_quantity, False, ) else: assert self.buyers[step].controller is None self.buyers[step] = ControllerInfo( controller, step, is_seller, self._trange(step, is_seller), target, expected_quantity, False, ) return controller
def _urange(self, step, is_seller, time_range): if is_seller: cprice = self.awi.catalog_prices[self.output_product] if self.adapt_prices: cprice = self.output_price[time_range[0] : time_range[1]] if len(cprice): cprice = int(cprice.mean()) else: cprice = self.awi.catalog_prices[self.output_product] return cprice, 2 * cprice cprice = self.awi.catalog_prices[self.input_product] if self.adapt_prices: cprice = self.input_cost[time_range[0] : time_range[1]] if len(cprice): cprice = int(cprice.mean()) else: cprice = self.awi.catalog_prices[self.input_product] return 1, cprice def _trange(self, step, is_seller): if is_seller: return max(step, self.awi.current_step + 1), self.awi.n_steps - 1 return self.awi.current_step + 1, step - 1 def _get_controller(self, mechanism) -> StepController: neg = self._running_negotiations[mechanism.id] return neg.negotiator.parent
# if neg.my_request: # controller_index = neg.extra["controller_index"] # if neg.extra["is_seller"]: # return self.sellers[controller_index].controller # else: # return self.buyers[controller_index].controller # else: